♻️ refactor: remove llm page (#9940)

remove llm page
This commit is contained in:
Arvin Xu
2025-10-31 01:14:36 +08:00
committed by GitHub
parent 39a80c5604
commit 6ec01a3c83
21 changed files with 0 additions and 1587 deletions

View File

@@ -15,9 +15,6 @@ const componentMap = {
[SettingsTabs.Agent]: dynamic(() => import('../agent'), {
loading: () => <Loading />,
}),
[SettingsTabs.LLM]: dynamic(() => import('../llm'), {
loading: () => <Loading />,
}),
[SettingsTabs.Provider]: dynamic(() => import('../provider'), {
loading: () => <Loading />,
}),

View File

@@ -1,93 +0,0 @@
'use client';
import { AutoComplete, Input, InputPassword, Markdown } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { ModelProvider } from 'model-bank';
import { useTranslation } from 'react-i18next';
import { AzureProviderCard } from '@/config/modelProviders';
import { useUserStore } from '@/store/user';
import { modelProviderSelectors } from '@/store/user/selectors';
import { KeyVaultsConfigKey, LLMProviderApiTokenKey } from '../../const';
import { ProviderItem } from '../../type';
const useStyles = createStyles(({ css, token }) => ({
markdown: css`
p {
color: ${token.colorTextDescription} !important;
}
`,
tip: css`
font-size: 12px;
color: ${token.colorTextDescription};
`,
}));
const providerKey = ModelProvider.Azure;
export const useAzureProvider = (): ProviderItem => {
const { t } = useTranslation('modelProvider');
const { styles } = useStyles();
// Get the first model card's deployment name as the check model
const checkModel = useUserStore((s) => {
const chatModelCards = modelProviderSelectors.getModelCardsById(providerKey)(s);
if (chatModelCards.length > 0) {
return chatModelCards[0].deploymentName;
}
return 'gpt-35-turbo';
});
return {
...AzureProviderCard,
apiKeyItems: [
{
children: (
<InputPassword autoComplete={'new-password'} placeholder={t('azure.token.placeholder')} />
),
desc: t('azure.token.desc'),
label: t('azure.token.title'),
name: [KeyVaultsConfigKey, providerKey, LLMProviderApiTokenKey],
},
{
children: <Input allowClear placeholder={t('azure.endpoint.placeholder')} />,
desc: t('azure.endpoint.desc'),
label: t('azure.endpoint.title'),
name: [KeyVaultsConfigKey, providerKey, 'endpoint'],
},
{
children: (
<AutoComplete
options={[
'2024-06-01',
'2024-02-01',
'2024-05-01-preview',
'2024-04-01-preview',
'2024-03-01-preview',
'2024-02-15-preview',
'2023-10-01-preview',
'2023-06-01-preview',
'2023-05-15',
].map((i) => ({ label: i, value: i }))}
placeholder={'20XX-XX-XX'}
/>
),
desc: (
<Markdown className={styles.markdown} fontSize={12} variant={'chat'}>
{t('azure.azureApiVersion.desc')}
</Markdown>
),
label: t('azure.azureApiVersion.title'),
name: [KeyVaultsConfigKey, providerKey, 'apiVersion'],
},
],
checkModel,
modelList: {
azureDeployName: true,
notFoundContent: t('azure.empty'),
placeholder: t('azure.modelListPlaceholder'),
},
};
};

View File

@@ -1,70 +0,0 @@
'use client';
import { InputPassword, Select } from '@lobehub/ui';
import { useTranslation } from 'react-i18next';
import { BedrockProviderCard } from '@/config/modelProviders';
import { GlobalLLMProviderKey } from '@/types/user/settings';
import { KeyVaultsConfigKey } from '../../const';
import { ProviderItem } from '../../type';
const providerKey: GlobalLLMProviderKey = 'bedrock';
export const useBedrockProvider = (): ProviderItem => {
const { t } = useTranslation('modelProvider');
return {
...BedrockProviderCard,
apiKeyItems: [
{
children: (
<InputPassword
autoComplete={'new-password'}
placeholder={t(`${providerKey}.accessKeyId.placeholder`)}
/>
),
desc: t(`${providerKey}.accessKeyId.desc`),
label: t(`${providerKey}.accessKeyId.title`),
name: [KeyVaultsConfigKey, providerKey, 'accessKeyId'],
},
{
children: (
<InputPassword
autoComplete={'new-password'}
placeholder={t(`${providerKey}.secretAccessKey.placeholder`)}
/>
),
desc: t(`${providerKey}.secretAccessKey.desc`),
label: t(`${providerKey}.secretAccessKey.title`),
name: [KeyVaultsConfigKey, providerKey, 'secretAccessKey'],
},
{
children: (
<InputPassword
autoComplete={'new-password'}
placeholder={t(`${providerKey}.sessionToken.placeholder`)}
/>
),
desc: t(`${providerKey}.sessionToken.desc`),
label: t(`${providerKey}.sessionToken.title`),
name: [KeyVaultsConfigKey, providerKey, 'sessionToken'],
},
{
children: (
<Select
allowClear
options={['us-east-1', 'us-west-2', 'ap-southeast-1', 'eu-central-1'].map((i) => ({
label: i,
value: i,
}))}
placeholder={'us-east-1'}
/>
),
desc: t(`${providerKey}.region.desc`),
label: t(`${providerKey}.region.title`),
name: [KeyVaultsConfigKey, providerKey, 'region'],
},
],
};
};

View File

@@ -1,39 +0,0 @@
'use client';
import { Input, InputPassword } from '@lobehub/ui';
import { useTranslation } from 'react-i18next';
import { CloudflareProviderCard } from '@/config/modelProviders';
import { GlobalLLMProviderKey } from '@/types/user/settings';
import { KeyVaultsConfigKey } from '../../const';
import { ProviderItem } from '../../type';
const providerKey: GlobalLLMProviderKey = 'cloudflare';
export const useCloudflareProvider = (): ProviderItem => {
const { t } = useTranslation('modelProvider');
return {
...CloudflareProviderCard,
apiKeyItems: [
{
children: (
<InputPassword
autoComplete={'new-password'}
placeholder={t(`${providerKey}.apiKey.placeholder`)}
/>
),
desc: t(`${providerKey}.apiKey.desc`),
label: t(`${providerKey}.apiKey.title`),
name: [KeyVaultsConfigKey, providerKey, 'apiKey'],
},
{
children: <Input placeholder={t(`${providerKey}.baseURLOrAccountID.placeholder`)} />,
desc: t(`${providerKey}.baseURLOrAccountID.desc`),
label: t(`${providerKey}.baseURLOrAccountID.title`),
name: [KeyVaultsConfigKey, providerKey, 'baseURLOrAccountID'],
},
],
};
};

View File

@@ -1,52 +0,0 @@
'use client';
import { InputPassword, Markdown } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { useTranslation } from 'react-i18next';
import { GithubProviderCard } from '@/config/modelProviders';
import { GlobalLLMProviderKey } from '@/types/user/settings';
import { KeyVaultsConfigKey, LLMProviderApiTokenKey } from '../../const';
import { ProviderItem } from '../../type';
const useStyles = createStyles(({ css, token }) => ({
markdown: css`
p {
color: ${token.colorTextDescription} !important;
}
`,
tip: css`
font-size: 12px;
color: ${token.colorTextDescription};
`,
}));
const providerKey: GlobalLLMProviderKey = 'github';
// Same as OpenAIProvider, but replace API Key with Github Personal Access Token
export const useGithubProvider = (): ProviderItem => {
const { t } = useTranslation('modelProvider');
const { styles } = useStyles();
return {
...GithubProviderCard,
apiKeyItems: [
{
children: (
<InputPassword
autoComplete={'new-password'}
placeholder={t(`${providerKey}.personalAccessToken.placeholder`)}
/>
),
desc: (
<Markdown className={styles.markdown} fontSize={12} variant={'chat'}>
{t(`${providerKey}.personalAccessToken.desc`)}
</Markdown>
),
label: t(`${providerKey}.personalAccessToken.title`),
name: [KeyVaultsConfigKey, providerKey, LLMProviderApiTokenKey],
},
],
};
};

View File

@@ -1,52 +0,0 @@
'use client';
import { GlobalLLMProviderKey } from '@lobechat/types';
import { InputPassword, Markdown } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { useTranslation } from 'react-i18next';
import { HuggingFaceProviderCard } from '@/config/modelProviders';
import { KeyVaultsConfigKey, LLMProviderApiTokenKey } from '../../const';
import { ProviderItem } from '../../type';
const useStyles = createStyles(({ css, token }) => ({
markdown: css`
p {
color: ${token.colorTextDescription} !important;
}
`,
tip: css`
font-size: 12px;
color: ${token.colorTextDescription};
`,
}));
const providerKey: GlobalLLMProviderKey = 'huggingface';
// Same as OpenAIProvider, but replace API Key with HuggingFace Access Token
export const useHuggingFaceProvider = (): ProviderItem => {
const { t } = useTranslation('modelProvider');
const { styles } = useStyles();
return {
...HuggingFaceProviderCard,
apiKeyItems: [
{
children: (
<InputPassword
autoComplete={'new-password'}
placeholder={t(`${providerKey}.accessToken.placeholder`)}
/>
),
desc: (
<Markdown className={styles.markdown} fontSize={12} variant={'chat'}>
{t(`${providerKey}.accessToken.desc`)}
</Markdown>
),
label: t(`${providerKey}.accessToken.title`),
name: [KeyVaultsConfigKey, providerKey, LLMProviderApiTokenKey],
},
],
};
};

View File

@@ -1,20 +0,0 @@
'use client';
import { useTranslation } from 'react-i18next';
import { OllamaProviderCard } from '@/config/modelProviders';
import { ProviderItem } from '../../type';
export const useOllamaProvider = (): ProviderItem => {
const { t } = useTranslation('modelProvider');
return {
...OllamaProviderCard,
proxyUrl: {
desc: t('ollama.endpoint.desc'),
placeholder: 'http://127.0.0.1:11434',
title: t('ollama.endpoint.title'),
},
};
};

View File

@@ -1,17 +0,0 @@
'use client';
import { OpenAIProviderCard } from '@/config/modelProviders';
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
import { ProviderItem } from '../../type';
export const useOpenAIProvider = (): ProviderItem => {
const { showOpenAIProxyUrl, showOpenAIApiKey } = useServerConfigStore(featureFlagsSelectors);
return {
...OpenAIProviderCard,
proxyUrl: showOpenAIProxyUrl && {
placeholder: 'https://api.openai.com/v1',
},
showApiKey: showOpenAIApiKey,
};
};

View File

@@ -1,132 +0,0 @@
import { useMemo } from 'react';
import {
Ai21ProviderCard,
Ai302ProviderCard,
Ai360ProviderCard,
AkashChatProviderCard,
AnthropicProviderCard,
BaichuanProviderCard,
CohereProviderCard,
DeepSeekProviderCard,
FireworksAIProviderCard,
GiteeAIProviderCard,
GoogleProviderCard,
GroqProviderCard,
HigressProviderCard,
HunyuanProviderCard,
InfiniAIProviderCard,
InternLMProviderCard,
JinaProviderCard,
MinimaxProviderCard,
MistralProviderCard,
MoonshotProviderCard,
NovitaProviderCard,
NvidiaProviderCard,
OllamaCloudProviderCard,
OpenRouterProviderCard,
PPIOProviderCard,
PerplexityProviderCard,
QiniuProviderCard,
QwenProviderCard,
SambaNovaProviderCard,
Search1APIProviderCard,
SenseNovaProviderCard,
SiliconCloudProviderCard,
SparkProviderCard,
StepfunProviderCard,
TaichuProviderCard,
TogetherAIProviderCard,
UpstageProviderCard,
V0ProviderCard,
VLLMProviderCard,
WenxinProviderCard,
XAIProviderCard,
XinferenceProviderCard,
ZeroOneProviderCard,
ZhiPuProviderCard,
} from '@/config/modelProviders';
import { ProviderItem } from '../type';
import { useAzureProvider } from './Azure';
import { useBedrockProvider } from './Bedrock';
import { useCloudflareProvider } from './Cloudflare';
import { useGithubProvider } from './Github';
import { useHuggingFaceProvider } from './HuggingFace';
import { useOllamaProvider } from './Ollama';
import { useOpenAIProvider } from './OpenAI';
export const useProviderList = (): ProviderItem[] => {
const AzureProvider = useAzureProvider();
const OllamaProvider = useOllamaProvider();
const OpenAIProvider = useOpenAIProvider();
const BedrockProvider = useBedrockProvider();
const CloudflareProvider = useCloudflareProvider();
const GithubProvider = useGithubProvider();
const HuggingFaceProvider = useHuggingFaceProvider();
return useMemo(
() => [
OpenAIProvider,
AzureProvider,
OllamaProvider,
VLLMProviderCard,
XinferenceProviderCard,
AnthropicProviderCard,
BedrockProvider,
GoogleProviderCard,
DeepSeekProviderCard,
HuggingFaceProvider,
OpenRouterProviderCard,
CloudflareProvider,
GithubProvider,
NovitaProviderCard,
TogetherAIProviderCard,
FireworksAIProviderCard,
GroqProviderCard,
NvidiaProviderCard,
PerplexityProviderCard,
MistralProviderCard,
Ai21ProviderCard,
UpstageProviderCard,
XAIProviderCard,
JinaProviderCard,
SambaNovaProviderCard,
Search1APIProviderCard,
CohereProviderCard,
V0ProviderCard,
QiniuProviderCard,
QwenProviderCard,
WenxinProviderCard,
HunyuanProviderCard,
SparkProviderCard,
ZhiPuProviderCard,
ZeroOneProviderCard,
SenseNovaProviderCard,
StepfunProviderCard,
MoonshotProviderCard,
BaichuanProviderCard,
MinimaxProviderCard,
Ai360ProviderCard,
TaichuProviderCard,
InternLMProviderCard,
SiliconCloudProviderCard,
HigressProviderCard,
GiteeAIProviderCard,
PPIOProviderCard,
InfiniAIProviderCard,
AkashChatProviderCard,
Ai302ProviderCard,
OllamaCloudProviderCard,
],
[
AzureProvider,
OllamaProvider,
OpenAIProvider,
BedrockProvider,
CloudflareProvider,
GithubProvider,
HuggingFaceProvider,
],
);
};

View File

@@ -1,118 +0,0 @@
'use client';
import { CheckCircleFilled } from '@ant-design/icons';
import { ChatMessageError, TraceNameMap } from '@lobechat/types';
import { Alert, Button, Highlighter } from '@lobehub/ui';
import { useTheme } from 'antd-style';
import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import { useIsMobile } from '@/hooks/useIsMobile';
import { useProviderName } from '@/hooks/useProviderName';
import { chatService } from '@/services/chat';
interface ConnectionCheckerProps {
model: string;
provider: string;
}
const Error = memo<{ error: ChatMessageError }>(({ error }) => {
const { t } = useTranslation('error');
const providerName = useProviderName(error.body?.provider);
return (
<Flexbox gap={8} style={{ maxWidth: '600px', width: '100%' }}>
<Alert
banner
extra={
<Flexbox>
<Highlighter actionIconSize={'small'} language={'json'} variant={'borderless'}>
{JSON.stringify(error.body || error, null, 2)}
</Highlighter>
</Flexbox>
}
message={t(`response.${error.type}` as any, { provider: providerName })}
showIcon
type={'error'}
/>
</Flexbox>
);
});
const Checker = memo<ConnectionCheckerProps>(({ model, provider }) => {
const { t } = useTranslation('setting');
const [loading, setLoading] = useState(false);
const [pass, setPass] = useState(false);
const theme = useTheme();
const [error, setError] = useState<ChatMessageError | undefined>();
const checkConnection = async () => {
let isError = false;
await chatService.fetchPresetTaskResult({
onError: (_, rawError) => {
setError(rawError);
setPass(false);
isError = true;
},
onFinish: async (value) => {
if (!isError && value) {
setError(undefined);
setPass(true);
} else {
setPass(false);
setError({
body: value,
message: t('response.ConnectionCheckFailed', { ns: 'error' }),
type: 'ConnectionCheckFailed',
});
}
},
onLoadingChange: (loading) => {
setLoading(loading);
},
params: {
messages: [
{
content: '你好',
role: 'user',
},
],
model,
provider,
},
trace: {
sessionId: `connection:${provider}`,
topicId: model,
traceName: TraceNameMap.ConnectivityChecker,
},
});
};
const isMobile = useIsMobile();
return (
<Flexbox align={isMobile ? 'flex-start' : 'flex-end'} gap={8}>
<Flexbox align={'center'} direction={isMobile ? 'horizontal-reverse' : 'horizontal'} gap={12}>
{pass && (
<Flexbox gap={4} horizontal>
<CheckCircleFilled
style={{
color: theme.colorSuccess,
}}
/>
{t('llm.checker.pass')}
</Flexbox>
)}
<Button loading={loading} onClick={checkConnection}>
{t('llm.checker.button')}
</Button>
</Flexbox>
{error && <Error error={error} />}
</Flexbox>
);
});
export default Checker;

View File

@@ -1,303 +0,0 @@
'use client';
import { ProviderCombine } from '@lobehub/icons';
import {
Form,
type FormGroupItemType,
type FormItemProps,
Icon,
Input,
InputPassword,
Tooltip,
} from '@lobehub/ui';
import { Switch } from 'antd';
import { createStyles } from 'antd-style';
import { debounce } from 'lodash-es';
import { LockIcon } from 'lucide-react';
import Link from 'next/link';
import { ReactNode, memo } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { Center, Flexbox } from 'react-layout-kit';
import urlJoin from 'url-join';
import { useSyncSettings } from '@/app/[variants]/(main)/settings/hooks/useSyncSettings';
import {
KeyVaultsConfigKey,
LLMProviderApiTokenKey,
LLMProviderBaseUrlKey,
LLMProviderConfigKey,
LLMProviderModelListKey,
} from '@/app/[variants]/(main)/settings/llm/const';
import { FORM_STYLE } from '@/const/layoutTokens';
import { AES_GCM_URL, BASE_PROVIDER_DOC_URL } from '@/const/url';
import { isServerMode } from '@/const/version';
import { useUserStore } from '@/store/user';
import { keyVaultsConfigSelectors, modelConfigSelectors } from '@/store/user/selectors';
import { ModelProviderCard } from '@/types/llm';
import { GlobalLLMProviderKey } from '@/types/user/settings';
import Checker from '../Checker';
import ProviderModelListSelect from '../ProviderModelList';
const useStyles = createStyles(({ css, prefixCls, responsive, token }) => ({
aceGcm: css`
padding-block: 0 !important;
.${prefixCls}-form-item-label {
display: none;
}
.${prefixCls}-form-item-control {
width: 100%;
font-size: 12px;
color: ${token.colorTextSecondary};
text-align: center;
opacity: 0.66;
transition: opacity 0.2s ${token.motionEaseInOut};
&:hover {
opacity: 1;
}
}
`,
form: css`
.${prefixCls}-form-item-control:has(.${prefixCls}-input,.${prefixCls}-select) {
flex: none;
width: min(70%, 800px);
min-width: min(70%, 800px) !important;
}
${responsive.mobile} {
width: 100%;
min-width: unset !important;
}
.${prefixCls}-select-selection-overflow-item {
font-size: 12px;
}
`,
help: css`
border-radius: 50%;
font-size: 12px;
font-weight: 500;
color: ${token.colorTextDescription};
background: ${token.colorFillTertiary};
&:hover {
color: ${token.colorText};
background: ${token.colorFill};
}
`,
}));
export interface ProviderConfigProps extends Omit<ModelProviderCard, 'id' | 'chatModels'> {
apiKeyItems?: FormItemProps[];
canDeactivate?: boolean;
checkerItem?: FormItemProps;
className?: string;
extra?: ReactNode;
hideSwitch?: boolean;
id: GlobalLLMProviderKey;
modelList?: {
azureDeployName?: boolean;
notFoundContent?: ReactNode;
placeholder?: string;
showModelFetcher?: boolean;
};
showAceGcm?: boolean;
title?: ReactNode;
}
const ProviderConfig = memo<ProviderConfigProps>(
({
apiKeyItems,
id,
proxyUrl,
showApiKey = true,
checkModel,
canDeactivate = true,
checkerItem,
modelList,
title,
defaultShowBrowserRequest,
disableBrowserRequest,
className,
name,
showAceGcm = true,
showChecker = true,
extra,
}) => {
const { t } = useTranslation('setting');
const [form] = Form.useForm();
const { cx, styles } = useStyles();
const [
toggleProviderEnabled,
setSettings,
enabled,
isFetchOnClient,
isProviderEndpointNotEmpty,
isProviderApiKeyNotEmpty,
] = useUserStore((s) => [
s.toggleProviderEnabled,
s.setSettings,
modelConfigSelectors.isProviderEnabled(id)(s),
modelConfigSelectors.isProviderFetchOnClient(id)(s),
keyVaultsConfigSelectors.isProviderEndpointNotEmpty(id)(s),
keyVaultsConfigSelectors.isProviderApiKeyNotEmpty(id)(s),
]);
useSyncSettings(form);
const apiKeyItem: FormItemProps[] = !showApiKey
? []
: (apiKeyItems ?? [
{
children: (
<InputPassword
autoComplete={'new-password'}
placeholder={t(`llm.apiKey.placeholder`, { name })}
/>
),
desc: t(`llm.apiKey.desc`, { name }),
label: t(`llm.apiKey.title`),
name: [KeyVaultsConfigKey, id, LLMProviderApiTokenKey],
},
]);
const aceGcmItem: FormItemProps = {
children: (
<>
<Icon icon={LockIcon} style={{ marginRight: 4 }} />
<Trans i18nKey="llm.aesGcm" ns={'setting'}>
使
<Link href={AES_GCM_URL} style={{ marginInline: 4 }} target={'_blank'}>
AES-GCM
</Link>
</Trans>
</>
),
className: styles.aceGcm,
minWidth: undefined,
};
const showEndpoint = !!proxyUrl;
const formItems = [
...apiKeyItem,
showEndpoint && {
children: <Input allowClear placeholder={proxyUrl?.placeholder} />,
desc: proxyUrl?.desc || t('llm.proxyUrl.desc'),
label: proxyUrl?.title || t('llm.proxyUrl.title'),
name: [KeyVaultsConfigKey, id, LLMProviderBaseUrlKey],
},
/*
* Conditions to show Client Fetch Switch
* 1. provider is not disabled browser request
* 2. provider show browser request by default
* 3. Provider allow to edit endpoint and the value of endpoint is not empty
* 4. There is an apikey provided by user
*/
!disableBrowserRequest &&
(defaultShowBrowserRequest ||
(showEndpoint && isProviderEndpointNotEmpty) ||
(showApiKey && isProviderApiKeyNotEmpty)) && {
children: (
<Switch
onChange={(enabled) => {
setSettings({ [LLMProviderConfigKey]: { [id]: { fetchOnClient: enabled } } });
}}
value={isFetchOnClient}
/>
),
desc: t('llm.fetchOnClient.desc'),
label: t('llm.fetchOnClient.title'),
minWidth: undefined,
},
{
children: (
<ProviderModelListSelect
notFoundContent={modelList?.notFoundContent}
placeholder={modelList?.placeholder ?? t('llm.modelList.placeholder')}
provider={id}
showAzureDeployName={modelList?.azureDeployName}
showModelFetcher={modelList?.showModelFetcher}
/>
),
desc: t('llm.modelList.desc'),
label: t('llm.modelList.title'),
name: [LLMProviderConfigKey, id, LLMProviderModelListKey],
},
showChecker
? (checkerItem ?? {
children: <Checker model={checkModel!} provider={id} />,
desc: t('llm.checker.desc'),
label: t('llm.checker.title'),
minWidth: undefined,
})
: undefined,
showAceGcm && isServerMode && aceGcmItem,
].filter(Boolean) as FormItemProps[];
/* ↓ cloud slot ↓ */
/* ↑ cloud slot ↑ */
const model: FormGroupItemType = {
children: formItems,
defaultActive: canDeactivate ? enabled : undefined,
extra: (
<Flexbox align={'center'} gap={8} horizontal>
{extra}
<Tooltip title={t('llm.helpDoc')}>
<Link
href={urlJoin(BASE_PROVIDER_DOC_URL, id)}
onClick={(e) => e.stopPropagation()}
target={'_blank'}
>
<Center className={styles.help} height={20} width={20}>
?
</Center>
</Link>
</Tooltip>
{canDeactivate ? (
<Switch
onChange={(enabled) => {
toggleProviderEnabled(id, enabled);
}}
value={enabled}
/>
) : undefined}
</Flexbox>
),
title: (
<Flexbox
align={'center'}
horizontal
style={{
height: 24,
maxHeight: 24,
...(enabled ? {} : { filter: 'grayscale(100%)', maxHeight: 24, opacity: 0.66 }),
}}
>
{title ?? <ProviderCombine provider={id} size={24} />}
</Flexbox>
),
};
return (
<Form
className={cx(styles.form, className)}
form={form}
items={[model]}
onValuesChange={debounce(setSettings, 100)}
{...FORM_STYLE}
/>
);
},
);
export default ProviderConfig;

View File

@@ -1,98 +0,0 @@
import { ModelIcon } from '@lobehub/icons';
import { ActionIcon, Icon, Text } from '@lobehub/ui';
import { App } from 'antd';
import isEqual from 'fast-deep-equal';
import { LucideArrowRight, LucideSettings, LucideTrash2 } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import { ModelInfoTags } from '@/components/ModelSelect';
import { useUserStore } from '@/store/user';
import { modelConfigSelectors, modelProviderSelectors } from '@/store/user/selectors';
import { GlobalLLMProviderKey } from '@/types/user/settings';
interface CustomModelOptionProps {
id: string;
provider: GlobalLLMProviderKey;
}
const CustomModelOption = memo<CustomModelOptionProps>(({ id, provider }) => {
const { t } = useTranslation('common');
const { t: s } = useTranslation('setting');
const { modal } = App.useApp();
const [dispatchCustomModelCards, toggleEditingCustomModelCard, removeEnabledModels] =
useUserStore((s) => [
s.dispatchCustomModelCards,
s.toggleEditingCustomModelCard,
s.removeEnabledModels,
]);
const modelCard = useUserStore(
modelConfigSelectors.getCustomModelCard({ id, provider }),
isEqual,
);
const isEnabled = useUserStore(
(s) => modelProviderSelectors.getEnableModelsById(provider)(s)?.includes(id),
isEqual,
);
return (
<Flexbox align={'center'} distribution={'space-between'} gap={8} horizontal>
<Flexbox align={'center'} gap={8} horizontal style={{ flex: 1, width: '70%' }}>
<ModelIcon model={id} size={32} />
<Flexbox direction={'vertical'} style={{ flex: 1, overflow: 'hidden' }}>
<Flexbox align={'center'} gap={8} horizontal>
<Text ellipsis>{modelCard?.displayName || id}</Text>
<ModelInfoTags id={id} {...modelCard} isCustom />
</Flexbox>
<Text ellipsis style={{ fontSize: 12, marginTop: '4px' }} type={'secondary'}>
{id}
{!!modelCard?.deploymentName && (
<>
<Icon icon={LucideArrowRight} />
{modelCard?.deploymentName}
</>
)}
</Text>
</Flexbox>
</Flexbox>
<Flexbox horizontal>
<ActionIcon
icon={LucideSettings}
onClick={async (e) => {
e.stopPropagation();
toggleEditingCustomModelCard({ id, provider });
}}
title={s('llm.customModelCards.config')}
/>
<ActionIcon
icon={LucideTrash2}
onClick={async (e) => {
e.stopPropagation();
e.preventDefault();
await modal.confirm({
centered: true,
content: s('llm.customModelCards.confirmDelete'),
okButtonProps: { danger: true },
onOk: async () => {
// delete model and deactivate id
await dispatchCustomModelCards(provider, { id, type: 'delete' });
await removeEnabledModels(provider, id);
},
type: 'warning',
});
}}
style={isEnabled ? { marginRight: '10px' } : {}}
title={t('delete')}
/>
</Flexbox>
</Flexbox>
);
});
export default CustomModelOption;

View File

@@ -1,104 +0,0 @@
import { Input } from '@lobehub/ui';
import { Checkbox, Form, FormInstance } from 'antd';
import { memo, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import MaxTokenSlider from '@/components/MaxTokenSlider';
import { useIsMobile } from '@/hooks/useIsMobile';
import { ChatModelCard } from '@/types/llm';
interface ModelConfigFormProps {
initialValues?: ChatModelCard;
onFormInstanceReady: (instance: FormInstance) => void;
showAzureDeployName?: boolean;
}
const ModelConfigForm = memo<ModelConfigFormProps>(
({ showAzureDeployName, onFormInstanceReady, initialValues }) => {
const { t } = useTranslation('setting');
const [formInstance] = Form.useForm();
const isMobile = useIsMobile();
useEffect(() => {
onFormInstanceReady(formInstance);
}, []);
return (
<div
onClick={(e) => {
e.stopPropagation();
}}
onKeyDown={(e) => {
e.stopPropagation();
}}
>
<Form
colon={false}
form={formInstance}
initialValues={initialValues}
labelCol={{ span: 4 }}
style={{ marginTop: 16 }}
wrapperCol={isMobile ? { span: 18 } : { offset: 1, span: 18 }}
>
<Form.Item
extra={t('llm.customModelCards.modelConfig.id.extra')}
label={t('llm.customModelCards.modelConfig.id.title')}
name={'id'}
>
<Input placeholder={t('llm.customModelCards.modelConfig.id.placeholder')} />
</Form.Item>
{showAzureDeployName && (
<Form.Item
extra={t('llm.customModelCards.modelConfig.azureDeployName.extra')}
label={t('llm.customModelCards.modelConfig.azureDeployName.title')}
name={'deploymentName'}
>
<Input
placeholder={t('llm.customModelCards.modelConfig.azureDeployName.placeholder')}
/>
</Form.Item>
)}
<Form.Item
label={t('llm.customModelCards.modelConfig.displayName.title')}
name={'displayName'}
>
<Input placeholder={t('llm.customModelCards.modelConfig.displayName.placeholder')} />
</Form.Item>
<Form.Item
label={t('llm.customModelCards.modelConfig.tokens.title')}
name={'contextWindowTokens'}
>
<MaxTokenSlider />
</Form.Item>
<Form.Item
extra={t('llm.customModelCards.modelConfig.functionCall.extra')}
label={t('llm.customModelCards.modelConfig.functionCall.title')}
name={'functionCall'}
valuePropName={'checked'}
>
<Checkbox />
</Form.Item>
<Form.Item
extra={t('llm.customModelCards.modelConfig.vision.extra')}
label={t('llm.customModelCards.modelConfig.vision.title')}
name={'vision'}
valuePropName={'checked'}
>
<Checkbox />
</Form.Item>
<Form.Item
extra={t('llm.customModelCards.modelConfig.files.extra')}
label={t('llm.customModelCards.modelConfig.files.title')}
name={'files'}
valuePropName={'checked'}
>
<Checkbox />
</Form.Item>
</Form>
</div>
);
},
);
export default ModelConfigForm;

View File

@@ -1,77 +0,0 @@
import { Button, Modal } from '@lobehub/ui';
import { FormInstance } from 'antd';
import isEqual from 'fast-deep-equal';
import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useUserStore } from '@/store/user';
import { modelConfigSelectors } from '@/store/user/selectors';
import ModelConfigForm from './Form';
interface ModelConfigModalProps {
provider?: string;
showAzureDeployName?: boolean;
}
const ModelConfigModal = memo<ModelConfigModalProps>(({ showAzureDeployName, provider }) => {
const { t } = useTranslation('setting');
const { t: tc } = useTranslation('common');
const [formInstance, setFormInstance] = useState<FormInstance>();
const [open, id, editingProvider, dispatchCustomModelCards, toggleEditingCustomModelCard] =
useUserStore((s) => [
!!s.editingCustomCardModel && provider === s.editingCustomCardModel?.provider,
s.editingCustomCardModel?.id,
s.editingCustomCardModel?.provider,
s.dispatchCustomModelCards,
s.toggleEditingCustomModelCard,
]);
const modelCard = useUserStore(
modelConfigSelectors.getCustomModelCard({ id, provider: editingProvider }),
isEqual,
);
const closeModal = () => {
toggleEditingCustomModelCard(undefined);
};
return (
<Modal
destroyOnHidden
footer={[
<Button key="cancel" onClick={closeModal}>
{tc('cancel')}
</Button>,
<Button
key="ok"
onClick={() => {
if (!editingProvider || !id || !formInstance) return;
const data = formInstance.getFieldsValue();
dispatchCustomModelCards(editingProvider as any, { id, type: 'update', value: data });
closeModal();
}}
style={{ marginInlineStart: '16px' }}
type="primary"
>
{tc('ok')}
</Button>,
]}
maskClosable
onCancel={closeModal}
open={open}
title={t('llm.customModelCards.modelConfig.modalTitle')}
zIndex={1251} // Select is 1150
>
<ModelConfigForm
initialValues={modelCard}
onFormInstanceReady={setFormInstance}
showAzureDeployName={showAzureDeployName}
/>
</Modal>
);
});
export default ModelConfigModal;

View File

@@ -1,105 +0,0 @@
import { ActionIcon, Icon, Text, Tooltip } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import dayjs from 'dayjs';
import isEqual from 'fast-deep-equal';
import { CircleX, LucideLoaderCircle, LucideRefreshCcwDot } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import { useUserStore } from '@/store/user';
import {
modelConfigSelectors,
modelProviderSelectors,
settingsSelectors,
} from '@/store/user/selectors';
import { GlobalLLMProviderKey } from '@/types/user/settings';
const useStyles = createStyles(({ css, token }) => ({
hover: css`
cursor: pointer;
padding-block: 4px;
padding-inline: 8px;
border-radius: ${token.borderRadius}px;
transition: all 0.2s ease-in-out;
&:hover {
color: ${token.colorText};
background-color: ${token.colorFillSecondary};
}
`,
}));
interface ModelFetcherProps {
provider: GlobalLLMProviderKey;
}
const ModelFetcher = memo<ModelFetcherProps>(({ provider }) => {
const { styles } = useStyles();
const { t } = useTranslation('setting');
const [useFetchProviderModelList, clearObtainedModels] = useUserStore((s) => [
s.useFetchProviderModelList,
s.clearObtainedModels,
s.setModelProviderConfig,
]);
const enabledAutoFetch = useUserStore(modelConfigSelectors.isAutoFetchModelsEnabled(provider));
const latestFetchTime = useUserStore(
(s) => settingsSelectors.providerConfig(provider)(s)?.latestFetchTime,
);
const totalModels = useUserStore(
(s) => modelProviderSelectors.getModelCardsById(provider)(s).length,
);
const remoteModels = useUserStore(
modelProviderSelectors.remoteProviderModelCards(provider),
isEqual,
);
const { mutate, isValidating } = useFetchProviderModelList(provider, enabledAutoFetch);
return (
<Text style={{ fontSize: 12 }} type={'secondary'}>
<Flexbox align={'center'} gap={0} horizontal justify={'space-between'}>
<div style={{ display: 'flex', lineHeight: '24px' }}>
{t('llm.modelList.total', { count: totalModels })}
{remoteModels && remoteModels.length > 0 && (
<ActionIcon
icon={CircleX}
onClick={() => clearObtainedModels(provider)}
size={'small'}
title={t('llm.fetcher.clear')}
/>
)}
</div>
<Tooltip
styles={{ root: { pointerEvents: 'none' } }}
title={
latestFetchTime
? t('llm.fetcher.latestTime', {
time: dayjs(latestFetchTime).format('YYYY-MM-DD HH:mm:ss'),
})
: t('llm.fetcher.noLatestTime')
}
>
<Flexbox
align={'center'}
className={styles.hover}
gap={4}
horizontal
onClick={() => mutate()}
>
<Icon
icon={isValidating ? LucideLoaderCircle : LucideRefreshCcwDot}
size={'small'}
spin={isValidating}
/>
<div>{isValidating ? t('llm.fetcher.fetching') : t('llm.fetcher.fetch')}</div>
</Flexbox>
</Tooltip>
</Flexbox>
</Text>
);
});
export default ModelFetcher;

View File

@@ -1,68 +0,0 @@
import { ModelIcon } from '@lobehub/icons';
import { ActionIcon, Text, Tooltip } from '@lobehub/ui';
import { useTheme } from 'antd-style';
import isEqual from 'fast-deep-equal';
import { Recycle } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import { ModelInfoTags } from '@/components/ModelSelect';
import { useUserStore } from '@/store/user';
import { modelProviderSelectors } from '@/store/user/selectors';
import { GlobalLLMProviderKey } from '@/types/user/settings';
import CustomModelOption from './CustomModelOption';
interface OptionRenderProps {
displayName: string;
id: string;
isAzure?: boolean;
provider: GlobalLLMProviderKey;
removed?: boolean;
}
const OptionRender = memo<OptionRenderProps>(({ displayName, id, provider, isAzure, removed }) => {
const model = useUserStore(
(s) => modelProviderSelectors.getModelCardById(id, provider)(s),
isEqual,
);
const { t } = useTranslation('components');
const theme = useTheme();
// if there is isCustom, it means it is a user defined custom model
if (model?.isCustom || isAzure) return <CustomModelOption id={id} provider={provider} />;
return (
<Flexbox
align={'center'}
gap={8}
horizontal
justify={'space-between'}
style={{ paddingInlineEnd: 8 }}
>
<Flexbox align={'center'} gap={8} horizontal>
<ModelIcon model={id} size={32} />
<Flexbox>
<Flexbox align={'center'} gap={8} horizontal>
{displayName}
<ModelInfoTags directionReverse placement={'top'} {...model!} />
</Flexbox>
<Text style={{ fontSize: 12 }} type={'secondary'}>
{id}
</Text>
</Flexbox>
</Flexbox>
{removed && (
<Tooltip
placement={'top'}
style={{ pointerEvents: 'none' }}
styles={{ root: { maxWidth: 300 } }}
title={t('ModelSelect.removed')}
>
<ActionIcon icon={Recycle} style={{ color: theme.colorWarning }} />
</Tooltip>
)}
</Flexbox>
);
});
export default OptionRender;

View File

@@ -1,146 +0,0 @@
import { ActionIcon, Select } from '@lobehub/ui';
import { css, cx } from 'antd-style';
import isEqual from 'fast-deep-equal';
import { RotateCwIcon } from 'lucide-react';
import { ReactNode, memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import { useUserStore } from '@/store/user';
import { modelProviderSelectors } from '@/store/user/selectors';
import { GlobalLLMProviderKey } from '@/types/user/settings';
import ModelConfigModal from './ModelConfigModal';
import ModelFetcher from './ModelFetcher';
import OptionRender from './Option';
const styles = {
divStyle: css`
position: relative;
.ant-select-selector {
padding-inline-end: 50px !important;
}
`,
popup: css`
&.ant-select-dropdown {
.ant-select-item-option-selected {
font-weight: normal;
}
}
`,
reset: css`
position: absolute;
z-index: 20;
inset-block-start: 50%;
inset-inline-end: 28px;
transform: translateY(-50%);
`,
};
interface CustomModelSelectProps {
notFoundContent?: ReactNode;
placeholder?: string;
provider: GlobalLLMProviderKey;
showAzureDeployName?: boolean;
showModelFetcher?: boolean;
}
const ProviderModelListSelect = memo<CustomModelSelectProps>(
({ showModelFetcher = false, provider, showAzureDeployName, notFoundContent, placeholder }) => {
const { t } = useTranslation('common');
const { t: transSetting } = useTranslation('setting');
const [setModelProviderConfig, updateEnabledModels] = useUserStore((s) => [
s.setModelProviderConfig,
s.updateEnabledModels,
]);
const chatModelCards = useUserStore(
modelProviderSelectors.getModelCardsById(provider),
isEqual,
);
const defaultEnableModel = useUserStore(
modelProviderSelectors.getDefaultEnabledModelsById(provider),
isEqual,
);
const enabledModels = useUserStore(
modelProviderSelectors.getEnableModelsById(provider),
isEqual,
);
const showReset = !!enabledModels && !isEqual(defaultEnableModel, enabledModels);
return (
<>
<Flexbox gap={8}>
<div className={cx(styles.divStyle)}>
<div className={cx(styles.reset)}>
{showReset && (
<ActionIcon
icon={RotateCwIcon}
onClick={() => {
setModelProviderConfig(provider, { enabledModels: null });
}}
size={'small'}
title={t('reset')}
/>
)}
</div>
<Select
allowClear
mode="tags"
notFoundContent={notFoundContent}
onChange={(value, options) => {
updateEnabledModels(provider, value, options as any[]);
}}
optionFilterProp="label"
optionRender={({ label, value }) => {
// model is in the chatModels
if (chatModelCards.some((c) => c.id === value))
return (
<OptionRender
displayName={label as string}
id={value as string}
isAzure={showAzureDeployName}
provider={provider}
/>
);
if (enabledModels?.some((m) => value === m)) {
return (
<OptionRender
displayName={label as string}
id={value as string}
isAzure={showAzureDeployName}
provider={provider}
removed
/>
);
}
// model is defined by user in client
return (
<Flexbox align={'center'} gap={8} horizontal>
{transSetting('llm.customModelCards.addNew', { id: value })}
</Flexbox>
);
}}
options={chatModelCards.map((model) => ({
label: model.displayName || model.id,
value: model.id,
}))}
placeholder={placeholder}
popupClassName={cx(styles.popup)}
value={enabledModels ?? defaultEnableModel}
/>
</div>
{showModelFetcher && <ModelFetcher provider={provider} />}
</Flexbox>
<ModelConfigModal provider={provider} showAzureDeployName={showAzureDeployName} />
</>
);
},
);
export default ProviderModelListSelect;

View File

@@ -1,20 +0,0 @@
export const LLMProviderConfigKey = 'languageModel';
export const KeyVaultsConfigKey = 'keyVaults';
/**
* we use this key to define default api key
* equal GOOGLE_API_KEY or ZHIPU_API_KEY
*/
export const LLMProviderApiTokenKey = 'apiKey';
/**
* we use this key to define the baseURL
* equal OPENAI_PROXY_URL
*/
export const LLMProviderBaseUrlKey = 'baseURL';
/**
* we use this key to define the custom model name
* equal CUSTOM_MODELS
*/
export const LLMProviderModelListKey = 'enabledModels';

View File

@@ -1,35 +0,0 @@
'use client';
import { useTheme } from 'antd-style';
import Link from 'next/link';
import { memo } from 'react';
import { Trans } from 'react-i18next';
import { Center } from 'react-layout-kit';
import { MORE_MODEL_PROVIDER_REQUEST_URL } from '@/const/url';
const Footer = memo(() => {
const theme = useTheme();
return (
<Center
style={{
background: theme.colorFillQuaternary,
border: `1px dashed ${theme.colorFillSecondary}`,
borderRadius: theme.borderRadiusLG,
padding: 12,
}}
>
<div style={{ color: theme.colorTextSecondary, fontSize: 12, textAlign: 'center' }}>
<Trans i18nKey="llm.waitingForMore" ns={'setting'}>
<Link aria-label={'todo'} href={MORE_MODEL_PROVIDER_REQUEST_URL} target="_blank">
</Link>
</Trans>
</div>
</Center>
);
});
export default Footer;

View File

@@ -1,30 +0,0 @@
'use client';
import { isCustomBranding } from '@/const/version';
import { useProviderList } from './ProviderList/providers';
import ProviderConfig from './components/ProviderConfig';
import Footer from './features/Footer';
const Page = () => {
const list = useProviderList();
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 24,
}}
>
{list.map(({ id, ...res }) => (
<ProviderConfig id={id as any} key={id} {...res} />
))}
{!isCustomBranding && <Footer />}
</div>
);
};
Page.displayName = 'LlmSetting';
export default Page;

View File

@@ -1,5 +0,0 @@
import { ProviderConfigProps } from './components/ProviderConfig';
export interface ProviderItem extends Omit<ProviderConfigProps, 'id'> {
id: string;
}