feat: ai proxy v1 (#3898)

* feat: ai proxy v1

* perf: ai proxy channel crud

* feat: ai proxy logs

* feat: channel test

* doc

* update lock
This commit is contained in:
Archer
2025-02-27 09:56:52 +08:00
committed by GitHub
parent 3c382d1240
commit 81a06718d8
40 changed files with 2869 additions and 746 deletions

View File

@@ -13,6 +13,10 @@ ROOT_KEY=fdafasd
OPENAI_BASE_URL=https://api.openai.com/v1
# OpenAI API Key
CHAT_API_KEY=sk-xxxx
# ai proxy api
AIPROXY_API_ENDPOINT=https://xxx.come
AIPROXY_API_TOKEN=xxxxx
# 强制将图片转成 base64 传递给模型
MULTIPLE_DATA_TO_BASE64=true

View File

@@ -0,0 +1,128 @@
import { ModelProviderIdType } from '@fastgpt/global/core/ai/provider';
import { ChannelInfoType } from './type';
import { i18nT } from '@fastgpt/web/i18n/utils';
export enum ChannelStatusEnum {
ChannelStatusUnknown = 0,
ChannelStatusEnabled = 1,
ChannelStatusDisabled = 2,
ChannelStatusAutoDisabled = 3
}
export const ChannelStautsMap = {
[ChannelStatusEnum.ChannelStatusUnknown]: {
label: i18nT('account_model:channel_status_unknown'),
colorSchema: 'gray'
},
[ChannelStatusEnum.ChannelStatusEnabled]: {
label: i18nT('account_model:channel_status_enabled'),
colorSchema: 'green'
},
[ChannelStatusEnum.ChannelStatusDisabled]: {
label: i18nT('account_model:channel_status_disabled'),
colorSchema: 'red'
},
[ChannelStatusEnum.ChannelStatusAutoDisabled]: {
label: i18nT('account_model:channel_status_auto_disabled'),
colorSchema: 'gray'
}
};
export const defaultChannel: ChannelInfoType = {
id: 0,
status: ChannelStatusEnum.ChannelStatusEnabled,
type: 1,
created_at: 0,
models: [],
model_mapping: {},
key: '',
name: '',
base_url: '',
priority: 0
};
export const aiproxyIdMap: Record<number, { label: string; provider: ModelProviderIdType }> = {
1: {
label: 'OpenAI',
provider: 'OpenAI'
},
3: {
label: i18nT('account_model:azure'),
provider: 'OpenAI'
},
14: {
label: 'Anthropic',
provider: 'Claude'
},
12: {
label: 'Google Gemini(OpenAI)',
provider: 'Gemini'
},
24: {
label: 'Google Gemini',
provider: 'Gemini'
},
28: {
label: 'Mistral AI',
provider: 'MistralAI'
},
29: {
label: 'Groq',
provider: 'Groq'
},
17: {
label: '阿里云',
provider: 'Qwen'
},
40: {
label: '豆包',
provider: 'Doubao'
},
36: {
label: 'DeepSeek AI',
provider: 'DeepSeek'
},
13: {
label: '百度智能云 V2',
provider: 'Ernie'
},
15: {
label: '百度智能云',
provider: 'Ernie'
},
16: {
label: '智谱 AI',
provider: 'ChatGLM'
},
18: {
label: '讯飞星火',
provider: 'SparkDesk'
},
25: {
label: '月之暗面',
provider: 'Moonshot'
},
26: {
label: '百川智能',
provider: 'Baichuan'
},
27: {
label: 'MiniMax',
provider: 'MiniMax'
},
31: {
label: '零一万物',
provider: 'Yi'
},
32: {
label: '阶跃星辰',
provider: 'StepFun'
},
43: {
label: 'SiliconFlow',
provider: 'Siliconflow'
},
30: {
label: 'Ollama',
provider: 'Ollama'
}
};

View File

@@ -0,0 +1,47 @@
import { ChannelStatusEnum } from './constants';
export type ChannelInfoType = {
model_mapping: Record<string, any>;
key: string;
name: string;
base_url: string;
models: any[];
id: number;
status: ChannelStatusEnum;
type: number;
created_at: number;
priority: number;
};
// Channel api
export type ChannelListQueryType = {
page: number;
perPage: number;
};
export type ChannelListResponseType = ChannelInfoType[];
export type CreateChannelProps = {
type: number;
model_mapping: Record<string, any>;
key?: string;
name: string;
base_url: string;
models: string[];
};
// Log
export type ChannelLogListItemType = {
token_name: string;
model: string;
request_id: string;
id: number;
channel: number;
mode: number;
created_at: number;
request_at: number;
code: number;
prompt_tokens: number;
completion_tokens: number;
endpoint: string;
content?: string;
};

View File

@@ -0,0 +1,722 @@
import {
Box,
Flex,
HStack,
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tr,
Switch,
ModalBody,
Input,
ModalFooter,
Button,
ButtonProps
} from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import React, { useMemo, useRef, useState } from 'react';
import {
ModelProviderList,
ModelProviderIdType,
getModelProvider
} from '@fastgpt/global/core/ai/provider';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { ModelTypeEnum } from '@fastgpt/global/core/ai/model';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getSystemModelDefaultConfig, putSystemModel } from '@/web/core/ai/config';
import { SystemModelItemType } from '@fastgpt/service/core/ai/type';
import { useForm } from 'react-hook-form';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
import MyTextarea from '@/components/common/Textarea/MyTextarea';
import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { Prompt_CQJson, Prompt_ExtractJson } from '@fastgpt/global/core/ai/prompt/agent';
import MyModal from '@fastgpt/web/components/common/MyModal';
export const AddModelButton = ({
onCreate,
...props
}: { onCreate: (type: ModelTypeEnum) => void } & ButtonProps) => {
const { t } = useTranslation();
return (
<MyMenu
trigger="hover"
size="sm"
Button={<Button {...props}>{t('account:create_model')}</Button>}
menuList={[
{
children: [
{
label: t('common:model.type.chat'),
onClick: () => onCreate(ModelTypeEnum.llm)
},
{
label: t('common:model.type.embedding'),
onClick: () => onCreate(ModelTypeEnum.embedding)
},
{
label: t('common:model.type.tts'),
onClick: () => onCreate(ModelTypeEnum.tts)
},
{
label: t('common:model.type.stt'),
onClick: () => onCreate(ModelTypeEnum.stt)
},
{
label: t('common:model.type.reRank'),
onClick: () => onCreate(ModelTypeEnum.rerank)
}
]
}
]}
/>
);
};
const InputStyles = {
maxW: '300px',
bg: 'myGray.50',
w: '100%',
rows: 3
};
export const ModelEditModal = ({
modelData,
onSuccess,
onClose
}: {
modelData: SystemModelItemType;
onSuccess: () => void;
onClose: () => void;
}) => {
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const { register, getValues, setValue, handleSubmit, watch, reset } =
useForm<SystemModelItemType>({
defaultValues: modelData
});
const isCustom = !!modelData.isCustom;
const isLLMModel = modelData?.type === ModelTypeEnum.llm;
const isEmbeddingModel = modelData?.type === ModelTypeEnum.embedding;
const isTTSModel = modelData?.type === ModelTypeEnum.tts;
const isSTTModel = modelData?.type === ModelTypeEnum.stt;
const isRerankModel = modelData?.type === ModelTypeEnum.rerank;
const provider = watch('provider');
const providerData = useMemo(() => getModelProvider(provider), [provider]);
const providerList = useRef<{ label: any; value: ModelProviderIdType }[]>(
ModelProviderList.map((item) => ({
label: (
<HStack>
<Avatar src={item.avatar} w={'1rem'} />
<Box>{t(item.name as any)}</Box>
</HStack>
),
value: item.id
}))
);
const priceUnit = useMemo(() => {
if (isLLMModel || isEmbeddingModel) return '/ 1k Tokens';
if (isTTSModel) return `/ 1k ${t('common:unit.character')}`;
if (isSTTModel) return `/ 60 ${t('common:unit.seconds')}`;
return '';
}, [isLLMModel, isEmbeddingModel, isTTSModel, t, isSTTModel]);
const { runAsync: updateModel, loading: updatingModel } = useRequest2(
async (data: SystemModelItemType) => {
return putSystemModel({
model: data.model,
metadata: data
}).then(onSuccess);
},
{
onSuccess: () => {
onClose();
},
successToast: t('common:common.Success')
}
);
const [key, setKey] = useState(0);
const { runAsync: loadDefaultConfig, loading: loadingDefaultConfig } = useRequest2(
getSystemModelDefaultConfig,
{
onSuccess(res) {
reset({
...getValues(),
...res
});
setTimeout(() => {
setKey((prev) => prev + 1);
}, 0);
}
}
);
return (
<MyModal
iconSrc={'modal/edit'}
title={t('account:model.edit_model')}
isOpen
onClose={onClose}
maxW={['90vw', '80vw']}
w={'100%'}
h={'100%'}
>
<ModalBody>
<Flex gap={4} key={key}>
<TableContainer flex={'1'}>
<Table>
<Thead>
<Tr color={'myGray.600'}>
<Th fontSize={'xs'}>{t('account:model.param_name')}</Th>
<Th fontSize={'xs'}></Th>
</Tr>
</Thead>
<Tbody>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.model_id')}</Box>
<QuestionTip label={t('account:model.model_id_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
{isCustom ? (
<Input {...register('model', { required: true })} {...InputStyles} />
) : (
modelData?.model
)}
</Td>
</Tr>
<Tr>
<Td>{t('common:model.provider')}</Td>
<Td textAlign={'right'}>
<MySelect
value={provider}
onchange={(value) => setValue('provider', value)}
list={providerList.current}
{...InputStyles}
/>
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.alias')}</Box>
<QuestionTip label={t('account:model.alias_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Input {...register('name', { required: true })} {...InputStyles} />
</Td>
</Tr>
{priceUnit && feConfigs?.isPlus && (
<>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.charsPointsPrice')}</Box>
<QuestionTip label={t('account:model.charsPointsPrice_tip')} />
</HStack>
</Td>
<Td>
<Flex justify="flex-end">
<HStack w={'100%'} maxW={'300px'}>
<MyNumberInput
flex={'1 0 0'}
register={register}
name={'charsPointsPrice'}
step={0.01}
/>
<Box fontSize={'sm'}>{priceUnit}</Box>
</HStack>
</Flex>
</Td>
</Tr>
{isLLMModel && (
<>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.input_price')}</Box>
<QuestionTip label={t('account:model.input_price_tip')} />
</HStack>
</Td>
<Td>
<Flex justify="flex-end">
<HStack w={'100%'} maxW={'300px'}>
<MyNumberInput
flex={'1 0 0'}
register={register}
name={'inputPrice'}
step={0.01}
/>
<Box fontSize={'sm'}>{priceUnit}</Box>
</HStack>
</Flex>
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.output_price')}</Box>
<QuestionTip label={t('account:model.output_price_tip')} />
</HStack>
</Td>
<Td>
<Flex justify="flex-end">
<HStack w={'100%'} maxW={'300px'}>
<MyNumberInput
flex={'1 0 0'}
register={register}
name={'outputPrice'}
step={0.01}
/>
<Box fontSize={'sm'}>{priceUnit}</Box>
</HStack>
</Flex>
</Td>
</Tr>
</>
)}
</>
)}
{isLLMModel && (
<>
<Tr>
<Td>{t('common:core.ai.Max context')}</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<MyNumberInput
register={register}
isRequired
name="maxContext"
{...InputStyles}
/>
</Flex>
</Td>
</Tr>
<Tr>
<Td>{t('account:model.max_quote')}</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<MyNumberInput
register={register}
isRequired
name="quoteMaxToken"
{...InputStyles}
/>
</Flex>
</Td>
</Tr>
<Tr>
<Td>{t('common:core.chat.response.module maxToken')}</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<MyNumberInput register={register} name="maxResponse" {...InputStyles} />
</Flex>
</Td>
</Tr>
<Tr>
<Td>{t('account:model.max_temperature')}</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<MyNumberInput
register={register}
name="maxTemperature"
step={0.1}
{...InputStyles}
/>
</Flex>
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.show_top_p')}</Box>
</HStack>
</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('showTopP')} />
</Flex>
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.show_stop_sign')}</Box>
</HStack>
</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('showStopSign')} />
</Flex>
</Td>
</Tr>
<Tr>
<Td>{t('account:model.response_format')}</Td>
<Td textAlign={'right'}>
<JsonEditor
value={JSON.stringify(getValues('responseFormatList'), null, 2)}
resize
onChange={(e) => {
if (!e) {
setValue('responseFormatList', []);
return;
}
try {
setValue('responseFormatList', JSON.parse(e));
} catch (error) {
console.error(error);
}
}}
{...InputStyles}
/>
</Td>
</Tr>
</>
)}
{isEmbeddingModel && (
<>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.normalization')}</Box>
<QuestionTip label={t('account:model.normalization_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('normalization')} />
</Flex>
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.default_token')}</Box>
<QuestionTip label={t('account:model.default_token_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<MyNumberInput
register={register}
isRequired
name="defaultToken"
{...InputStyles}
/>
</Flex>
</Td>
</Tr>
<Tr>
<Td>{t('common:core.ai.Max context')}</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<MyNumberInput
register={register}
isRequired
name="maxToken"
{...InputStyles}
/>
</Flex>
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.defaultConfig')}</Box>
<QuestionTip label={t('account:model.defaultConfig_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<JsonEditor
value={JSON.stringify(getValues('defaultConfig'), null, 2)}
onChange={(e) => {
if (!e) {
setValue('defaultConfig', {});
return;
}
try {
setValue('defaultConfig', JSON.parse(e));
} catch (error) {
console.error(error);
}
}}
{...InputStyles}
/>
</Flex>
</Td>
</Tr>
</>
)}
{isTTSModel && (
<>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.voices')}</Box>
<QuestionTip label={t('account:model.voices_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<JsonEditor
value={JSON.stringify(getValues('voices'), null, 2)}
onChange={(e) => {
try {
setValue('voices', JSON.parse(e));
} catch (error) {
console.error(error);
}
}}
{...InputStyles}
/>
</Flex>
</Td>
</Tr>
</>
)}
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.request_url')}</Box>
<QuestionTip label={t('account:model.request_url_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Input {...register('requestUrl')} {...InputStyles} />
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.request_auth')}</Box>
<QuestionTip label={t('account:model.request_auth_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Input {...register('requestAuth')} {...InputStyles} />
</Td>
</Tr>
</Tbody>
</Table>
</TableContainer>
{isLLMModel && (
<TableContainer flex={'1'}>
<Table>
<Thead>
<Tr color={'myGray.600'}>
<Th fontSize={'xs'}>{t('account:model.param_name')}</Th>
<Th fontSize={'xs'}></Th>
</Tr>
</Thead>
<Tbody>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.tool_choice')}</Box>
<QuestionTip label={t('account:model.tool_choice_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('toolChoice')} />
</Flex>
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.function_call')}</Box>
<QuestionTip label={t('account:model.function_call_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('functionCall')} />
</Flex>
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.vision')}</Box>
<QuestionTip label={t('account:model.vision_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('vision')} />
</Flex>
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.reasoning')}</Box>
<QuestionTip label={t('account:model.reasoning_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('reasoning')} />
</Flex>
</Td>
</Tr>
{feConfigs?.isPlus && (
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.censor')}</Box>
<QuestionTip label={t('account:model.censor_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('censor')} />
</Flex>
</Td>
</Tr>
)}
<Tr>
<Td>{t('account:model.dataset_process')}</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('datasetProcess')} />
</Flex>
</Td>
</Tr>
<Tr>
<Td>{t('account:model.used_in_classify')}</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('usedInClassify')} />
</Flex>
</Td>
</Tr>
<Tr>
<Td>{t('account:model.used_in_extract_fields')}</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('usedInExtractFields')} />
</Flex>
</Td>
</Tr>
<Tr>
<Td>{t('account:model.used_in_tool_call')}</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('usedInToolCall')} />
</Flex>
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.default_system_chat_prompt')}</Box>
<QuestionTip label={t('account:model.default_system_chat_prompt_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<MyTextarea {...register('defaultSystemChatPrompt')} {...InputStyles} />
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.custom_cq_prompt')}</Box>
<QuestionTip
label={t('account:model.custom_cq_prompt_tip', { prompt: Prompt_CQJson })}
/>
</HStack>
</Td>
<Td textAlign={'right'}>
<MyTextarea {...register('customCQPrompt')} {...InputStyles} />
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.custom_extract_prompt')}</Box>
<QuestionTip
label={t('account:model.custom_extract_prompt_tip', {
prompt: Prompt_ExtractJson
})}
/>
</HStack>
</Td>
<Td textAlign={'right'}>
<MyTextarea {...register('customExtractPrompt')} {...InputStyles} />
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.default_config')}</Box>
<QuestionTip label={t('account:model.default_config_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<JsonEditor
value={JSON.stringify(getValues('defaultConfig'), null, 2)}
resize
onChange={(e) => {
if (!e) {
setValue('defaultConfig', {});
return;
}
try {
setValue('defaultConfig', JSON.parse(e));
} catch (error) {
console.error(error);
}
}}
{...InputStyles}
/>
</Td>
</Tr>
</Tbody>
</Table>
</TableContainer>
)}
</Flex>
</ModalBody>
<ModalFooter>
{!modelData.isCustom && (
<Button
isLoading={loadingDefaultConfig}
variant={'whiteBase'}
mr={4}
onClick={() => loadDefaultConfig(modelData.model)}
>
{t('account:reset_default')}
</Button>
)}
<Button variant={'whiteBase'} mr={4} onClick={onClose}>
{t('common:common.Cancel')}
</Button>
<Button isLoading={updatingModel} onClick={handleSubmit(updateModel)}>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default function Dom() {
return <></>;
}

View File

@@ -0,0 +1,499 @@
import { aiproxyIdMap } from '@/global/aiproxy/constants';
import { ChannelInfoType } from '@/global/aiproxy/type';
import {
Box,
BoxProps,
Button,
Flex,
Input,
MenuItemProps,
ModalBody,
ModalFooter,
useDisclosure,
Menu,
MenuButton,
MenuList,
MenuItem,
HStack,
useOutsideClick
} from '@chakra-ui/react';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { useTranslation } from 'next-i18next';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useForm } from 'react-hook-form';
import { AddModelButton } from '../AddModelBox';
import dynamic from 'next/dynamic';
import { SystemModelItemType } from '@fastgpt/service/core/ai/type';
import { ModelTypeEnum } from '@fastgpt/global/core/ai/model';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { getSystemModelList } from '@/web/core/ai/config';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getModelProvider } from '@fastgpt/global/core/ai/provider';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyAvatar from '@fastgpt/web/components/common/Avatar';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
import { getChannelProviders, postCreateChannel, putChannel } from '@/web/core/ai/channel';
import CopyBox from '@fastgpt/web/components/common/String/CopyBox';
const ModelEditModal = dynamic(() => import('../AddModelBox').then((mod) => mod.ModelEditModal));
const LabelStyles: BoxProps = {
fontSize: 'sm',
color: 'myGray.900',
flex: '0 0 70px'
};
const EditChannelModal = ({
defaultConfig,
onClose,
onSuccess
}: {
defaultConfig: ChannelInfoType;
onClose: () => void;
onSuccess: () => void;
}) => {
const { t } = useTranslation();
const { defaultModels } = useSystemStore();
const isEdit = defaultConfig.id !== 0;
const { register, handleSubmit, watch, setValue } = useForm({
defaultValues: defaultConfig
});
const providerType = watch('type');
const { data: providerList = [] } = useRequest2(
() =>
getChannelProviders().then((res) => {
return Object.entries(res)
.map(([key, value]) => {
const mapData = aiproxyIdMap[key as any] ?? {
label: value.name,
provider: 'Other'
};
const provider = getModelProvider(mapData.provider);
return {
order: provider.order,
defaultBaseUrl: value.defaultBaseUrl,
keyHelp: value.keyHelp,
icon: provider.avatar,
label: t(mapData.label as any),
value: Number(key)
};
})
.sort((a, b) => a.order - b.order);
}),
{
manual: false
}
);
const selectedProvider = useMemo(() => {
const res = providerList.find((item) => item.value === providerType);
return res;
}, [providerList, providerType]);
const [editModelData, setEditModelData] = useState<SystemModelItemType>();
const onCreateModel = (type: ModelTypeEnum) => {
const defaultModel = defaultModels[type];
setEditModelData({
...defaultModel,
model: '',
name: '',
charsPointsPrice: 0,
inputPrice: undefined,
outputPrice: undefined,
isCustom: true,
isActive: true,
// @ts-ignore
type
});
};
const models = watch('models');
const {
data: systemModelList = [],
runAsync: refreshSystemModelList,
loading: loadingModels
} = useRequest2(getSystemModelList, {
manual: false
});
const modelList = useMemo(() => {
const currentProvider = aiproxyIdMap[providerType]?.provider;
return systemModelList
.map((item) => {
const provider = getModelProvider(item.provider);
return {
provider: item.provider,
icon: provider.avatar,
label: item.model,
value: item.model
};
})
.sort((a, b) => {
// sort by provider, same provider first
if (a.provider === currentProvider && b.provider !== currentProvider) return -1;
if (a.provider !== currentProvider && b.provider === currentProvider) return 1;
return 0;
});
}, [providerType, systemModelList]);
const modelMapping = watch('model_mapping');
const { runAsync: onSubmit, loading: loadingCreate } = useRequest2(
(data: ChannelInfoType) => {
if (data.models.length === 0) {
return Promise.reject(t('account_model:selected_model_empty'));
}
return isEdit ? putChannel(data) : postCreateChannel(data);
},
{
onSuccess() {
onSuccess();
onClose();
},
successToast: isEdit ? t('common:common.Update Success') : t('common:common.Create Success'),
manual: true
}
);
const isLoading = loadingModels || loadingCreate;
return (
<>
<MyModal
isLoading={isLoading}
iconSrc={'modal/setting'}
title={t('account_model:edit_channel')}
onClose={onClose}
w={'100%'}
maxW={['90vw', '800px']}
>
<ModalBody>
{/* Chnnel name */}
<Box>
<FormLabel required {...LabelStyles}>
{t('account_model:channel_name')}
</FormLabel>
<Input mt={1} {...register('name', { required: true })} />
</Box>
{/* Provider */}
<Box alignItems={'center'} mt={4}>
<FormLabel required {...LabelStyles}>
{t('account_model:channel_type')}
</FormLabel>
<Box mt={1}>
<MySelect
list={providerList}
placeholder={t('account_model:select_provider_placeholder')}
value={providerType}
isSearch
onchange={(val) => {
setValue('type', val);
}}
/>
</Box>
</Box>
{/* Model */}
<Box mt={4}>
<Flex alignItems={'center'}>
<FormLabel required flex={'1 0 0'}>
{t('account_model:model')}({models.length})
</FormLabel>
<AddModelButton onCreate={onCreateModel} size={'sm'} variant={'outline'} />
<Button ml={2} size={'sm'} variant={'outline'} onClick={() => setValue('models', [])}>
{t('account_model:clear_model')}
</Button>
</Flex>
<Box mt={2}>
<MultipleSelect
value={models}
list={modelList}
onSelect={(val) => {
setValue('models', val);
}}
/>
</Box>
</Box>
{/* Mapping */}
<Box mt={4}>
<HStack>
<FormLabel>{t('account_model:mapping')}</FormLabel>
<QuestionTip label={t('account_model:mapping_tip')} />
</HStack>
<Box mt={2}>
<JsonEditor
value={JSON.stringify(modelMapping, null, 2)}
onChange={(val) => {
if (!val) {
setValue('model_mapping', {});
} else {
try {
setValue('model_mapping', JSON.parse(val));
} catch (error) {}
}
}}
/>
</Box>
</Box>
{/* url and key */}
<Box mt={4}>
<Flex alignItems={'center'}>
<FormLabel>{t('account_model:base_url')}</FormLabel>
{selectedProvider && (
<Flex alignItems={'center'} fontSize={'xs'}>
<Box>{'('}</Box>
<Box mr={1}>{t('account_model:default_url')}:</Box>
<CopyBox value={selectedProvider?.defaultBaseUrl || ''}>
{selectedProvider?.defaultBaseUrl || ''}
</CopyBox>
<Box>{')'}</Box>
</Flex>
)}
</Flex>
<Input
mt={1}
{...register('base_url')}
placeholder={selectedProvider?.defaultBaseUrl || 'https://api.openai.com/v1'}
/>
</Box>
<Box mt={4}>
<Flex alignItems={'center'}>
<FormLabel>{t('account_model:api_key')}</FormLabel>
{selectedProvider?.keyHelp && (
<Flex alignItems={'center'} fontSize={'xs'}>
<Box>{'('}</Box>
<Box mr={1}>{t('account_model:key_type')}</Box>
<Box>{selectedProvider.keyHelp}</Box>
<Box>{')'}</Box>
</Flex>
)}
</Flex>
<Input
mt={1}
{...register('key')}
placeholder={selectedProvider?.keyHelp || 'sk-1234567890'}
/>
</Box>
</ModalBody>
<ModalFooter>
<Button variant={'outline'} onClick={onClose} mr={4}>
{t('common:common.Cancel')}
</Button>
<Button variant={'primary'} onClick={handleSubmit(onSubmit)}>
{isEdit ? t('common:common.Update') : t('common:new_create')}
</Button>
</ModalFooter>
</MyModal>
{!!editModelData && (
<ModelEditModal
modelData={editModelData}
onSuccess={refreshSystemModelList}
onClose={() => setEditModelData(undefined)}
/>
)}
</>
);
};
export default EditChannelModal;
type SelectProps = {
list: {
icon?: string;
label: string;
value: string;
}[];
value: string[];
onSelect: (val: string[]) => void;
};
const menuItemStyles: MenuItemProps = {
borderRadius: 'sm',
py: 2,
display: 'flex',
alignItems: 'center',
_hover: {
backgroundColor: 'myGray.100'
},
_notLast: {
mb: 0.5
}
};
const MultipleSelect = ({ value = [], list = [], onSelect }: SelectProps) => {
const ref = useRef<HTMLDivElement>(null);
const BoxRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
const { copyData } = useCopyData();
const onclickItem = useCallback(
(val: string) => {
if (value.includes(val)) {
onSelect(value.filter((i) => i !== val));
} else {
onSelect([...value, val]);
BoxRef.current?.scrollTo({
top: BoxRef.current.scrollHeight
});
}
},
[value, onSelect]
);
const [search, setSearch] = useState('');
const filterUnSelected = useMemo(() => {
return list
.filter((item) => !value.includes(item.value))
.filter((item) => {
if (!search) return true;
const regx = new RegExp(search, 'i');
return regx.test(item.label);
});
}, [list, value, search]);
useOutsideClick({
ref,
handler: () => {
onClose();
}
});
return (
<Box ref={ref}>
<Menu autoSelect={false} isOpen={isOpen} strategy={'fixed'} matchWidth closeOnSelect={false}>
<Box
position={'relative'}
py={2}
borderRadius={'md'}
border={'base'}
userSelect={'none'}
cursor={'pointer'}
_active={{
transform: 'none'
}}
_hover={{
borderColor: 'primary.300'
}}
{...(isOpen
? {
boxShadow: '0px 0px 4px #A8DBFF',
borderColor: 'primary.500',
onClick: onClose
}
: {
onClick: () => {
onOpen();
setSearch('');
}
})}
>
<MenuButton zIndex={0} position={'absolute'} bottom={0} left={0} right={0} top={0} />
<Flex
ref={BoxRef}
position={'relative'}
alignItems={value.length === 0 ? 'center' : 'flex-start'}
gap={2}
px={2}
pb={0}
overflowY={'auto'}
maxH={'200px'}
>
{value.length === 0 ? (
<Box flex={'1 0 0'} color={'myGray.500'} fontSize={'xs'}>
{t('account_model:select_model_placeholder')}
</Box>
) : (
<Flex flex={'1 0 0'} alignItems={'center'} gap={2} flexWrap={'wrap'}>
{value.map((item) => (
<MyTag
key={item}
type="borderSolid"
colorSchema="gray"
bg={'myGray.150'}
color={'myGray.900'}
_hover={{
bg: 'myGray.250'
}}
onClick={(e) => {
e.stopPropagation();
copyData(item, t('account_model:copy_model_id_success'));
}}
>
<Box>{item}</Box>
<MyIcon
ml={0.5}
name={'common/closeLight'}
w={'14px'}
h={'14px'}
_hover={{
color: 'red.600'
}}
onClick={(e) => {
e.stopPropagation();
onclickItem(item);
}}
/>
</MyTag>
))}
{isOpen && (
<Input
key={'search'}
variant={'unstyled'}
w={'150px'}
h={'24px'}
autoFocus
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder={t('account_model:search_model')}
onClick={(e) => {
e.stopPropagation();
}}
/>
)}
</Flex>
)}
<MyIcon name={'core/chat/chevronDown'} color={'myGray.600'} w={4} h={4} />
</Flex>
</Box>
<MenuList
px={'6px'}
py={'6px'}
border={'1px solid #fff'}
boxShadow={
'0px 4px 10px 0px rgba(19, 51, 107, 0.10), 0px 0px 1px 0px rgba(19, 51, 107, 0.10);'
}
zIndex={99}
maxH={'40vh'}
overflowY={'auto'}
>
{filterUnSelected.map((item, i) => {
return (
<MenuItem
key={i}
color={'myGray.900'}
onClick={(e) => {
onclickItem(item.value);
}}
whiteSpace={'pre-wrap'}
fontSize={'sm'}
gap={2}
{...menuItemStyles}
>
{item.icon && <MyAvatar src={item.icon} w={'1rem'} borderRadius={'0'} />}
<Box flex={'1 0 0'}>{item.label}</Box>
</MenuItem>
);
})}
</MenuList>
</Menu>
</Box>
);
};

View File

@@ -0,0 +1,196 @@
import { getSystemModelList, getTestModel } from '@/web/core/ai/config';
import {
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Box,
Flex,
Button,
HStack,
ModalBody,
ModalFooter
} from '@chakra-ui/react';
import { getModelProvider } from '@fastgpt/global/core/ai/provider';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import React, { useRef, useState } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { batchRun } from '@fastgpt/global/common/fn/utils';
import { useToast } from '@fastgpt/web/hooks/useToast';
type ModelTestItem = {
label: React.ReactNode;
model: string;
status: 'waiting' | 'running' | 'success' | 'error';
message?: string;
duration?: number;
};
const ModelTest = ({ models, onClose }: { models: string[]; onClose: () => void }) => {
const { t } = useTranslation();
const { toast } = useToast();
const [testModelList, setTestModelList] = useState<ModelTestItem[]>([]);
const statusMap = useRef({
waiting: {
label: t('account_model:waiting_test'),
colorSchema: 'gray'
},
running: {
label: t('account_model:running_test'),
colorSchema: 'blue'
},
success: {
label: t('common:common.Success'),
colorSchema: 'green'
},
error: {
label: t('common:common.failed'),
colorSchema: 'red'
}
});
const { loading: loadingModels } = useRequest2(getSystemModelList, {
manual: false,
refreshDeps: [models],
onSuccess(res) {
const list = models
.map((model) => {
const modelData = res.find((item) => item.model === model);
if (!modelData) return null;
const provider = getModelProvider(modelData.provider);
return {
label: (
<HStack>
<MyIcon name={provider.avatar as any} w={'1rem'} />
<Box>{t(modelData.name as any)}</Box>
</HStack>
),
model: modelData.model,
status: 'waiting'
};
})
.filter(Boolean) as ModelTestItem[];
setTestModelList(list);
}
});
const { runAsync: onStartTest, loading: isTesting } = useRequest2(
async () => {
{
let errorNum = 0;
const testModel = async (model: string) => {
setTestModelList((prev) =>
prev.map((item) =>
item.model === model ? { ...item, status: 'running', message: '' } : item
)
);
const start = Date.now();
try {
await getTestModel(model);
const duration = Date.now() - start;
setTestModelList((prev) =>
prev.map((item) =>
item.model === model
? { ...item, status: 'success', duration: duration / 1000 }
: item
)
);
} catch (error) {
setTestModelList((prev) =>
prev.map((item) =>
item.model === model
? { ...item, status: 'error', message: getErrText(error) }
: item
)
);
errorNum++;
}
};
await batchRun(
testModelList.map((item) => item.model),
testModel,
5
);
if (errorNum > 0) {
toast({
status: 'warning',
title: t('account_model:test_failed', { num: errorNum })
});
}
}
},
{
refreshDeps: [testModelList]
}
);
console.log(testModelList);
return (
<MyModal
iconSrc={'core/chat/sendLight'}
isLoading={loadingModels}
title={t('account_model:model_test')}
w={'600px'}
isOpen
>
<ModalBody>
<TableContainer h={'100%'} overflowY={'auto'} fontSize={'sm'} maxH={'60vh'}>
<Table>
<Thead>
<Tr>
<Th>{t('account_model:model')}</Th>
<Th>{t('account_model:channel_status')}</Th>
</Tr>
</Thead>
<Tbody>
{testModelList.map((item) => {
const data = statusMap.current[item.status];
return (
<Tr key={item.model}>
<Td>{item.label}</Td>
<Td>
<Flex alignItems={'center'}>
<MyTag mr={1} type="borderSolid" colorSchema={data.colorSchema as any}>
{data.label}
</MyTag>
{item.message && <QuestionTip label={item.message} />}
{item.status === 'success' && item.duration && (
<Box fontSize={'sm'} color={'myGray.500'}>
{t('account_model:request_duration', {
duration: item.duration.toFixed(2)
})}
</Box>
)}
</Flex>
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</TableContainer>
</ModalBody>
<ModalFooter>
<Button mr={4} variant={'whiteBase'} onClick={onClose}>
{t('common:common.Cancel')}
</Button>
<Button isLoading={isTesting} variant={'primary'} onClick={onStartTest}>
{t('account_model:start_test', { num: testModelList.length })}
</Button>
</ModalFooter>
</MyModal>
);
};
export default ModelTest;

View File

@@ -0,0 +1,230 @@
import { deleteChannel, getChannelList, putChannel, putChannelStatus } from '@/web/core/ai/channel';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import React, { useState } from 'react';
import {
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Box,
Flex,
Button,
HStack
} from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import MyBox from '@fastgpt/web/components/common/MyBox';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import { useUserStore } from '@/web/support/user/useUserStore';
import { ChannelInfoType } from '@/global/aiproxy/type';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import {
aiproxyIdMap,
ChannelStatusEnum,
ChannelStautsMap,
defaultChannel
} from '@/global/aiproxy/constants';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import dynamic from 'next/dynamic';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
import { getModelProvider } from '@fastgpt/global/core/ai/provider';
import MyIcon from '@fastgpt/web/components/common/Icon';
const EditChannelModal = dynamic(() => import('./EditChannelModal'), { ssr: false });
const ModelTest = dynamic(() => import('./ModelTest'), { ssr: false });
const ChannelTable = ({ Tab }: { Tab: React.ReactNode }) => {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const isRoot = userInfo?.username === 'root';
const {
data: channelList = [],
runAsync: refreshChannelList,
loading: loadingChannelList
} = useRequest2(getChannelList, {
manual: false
});
const [editChannel, setEditChannel] = useState<ChannelInfoType>();
const { runAsync: updateChannel, loading: loadingUpdateChannel } = useRequest2(putChannel, {
manual: true,
onSuccess: () => {
refreshChannelList();
}
});
const { runAsync: updateChannelStatus, loading: loadingUpdateChannelStatus } = useRequest2(
putChannelStatus,
{
onSuccess: () => {
refreshChannelList();
}
}
);
const { runAsync: onDeleteChannel, loading: loadingDeleteChannel } = useRequest2(deleteChannel, {
manual: true,
onSuccess: () => {
refreshChannelList();
}
});
const [testModels, setTestModels] = useState<string[]>();
const isLoading =
loadingChannelList ||
loadingUpdateChannel ||
loadingDeleteChannel ||
loadingUpdateChannelStatus;
return (
<>
{isRoot && (
<Flex alignItems={'center'}>
{Tab}
<Box flex={1} />
<Button variant={'whiteBase'} mr={2} onClick={() => setEditChannel(defaultChannel)}>
{t('account_model:create_channel')}
</Button>
</Flex>
)}
<MyBox flex={'1 0 0'} h={0} isLoading={isLoading}>
<TableContainer h={'100%'} overflowY={'auto'} fontSize={'sm'}>
<Table>
<Thead>
<Tr>
<Th>ID</Th>
<Th>{t('account_model:channel_name')}</Th>
<Th>{t('account_model:channel_type')}</Th>
<Th>{t('account_model:channel_status')}</Th>
<Th>
{t('account_model:channel_priority')}
<QuestionTip label={t('account_model:channel_priority_tip')} />
</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{channelList.map((item) => {
const providerData = aiproxyIdMap[item.type];
const provider = getModelProvider(providerData?.provider);
return (
<Tr key={item.id} _hover={{ bg: 'myGray.100' }}>
<Td>{item.id}</Td>
<Td>{item.name}</Td>
<Td>
{providerData ? (
<HStack>
<MyIcon name={provider?.avatar as any} w={'1rem'} />
<Box>{t(providerData?.label as any)}</Box>
</HStack>
) : (
'Invalid provider'
)}
</Td>
<Td>
<MyTag
colorSchema={ChannelStautsMap[item.status]?.colorSchema as any}
type="borderFill"
>
{t(ChannelStautsMap[item.status]?.label as any) ||
t('account_model:channel_status_unknown')}
</MyTag>
</Td>
<Td>
<MyNumberInput
defaultValue={item.priority || 0}
min={0}
max={100}
h={'32px'}
w={'80px'}
onBlur={(e) => {
const val = (() => {
if (!e) return 0;
return e;
})();
updateChannel({
...item,
priority: val
});
}}
/>
</Td>
<Td>
<MyMenu
menuList={[
{
label: '',
children: [
{
icon: 'core/chat/sendLight',
label: t('account_model:model_test'),
onClick: () => setTestModels(item.models)
},
...(item.status === ChannelStatusEnum.ChannelStatusEnabled
? [
{
icon: 'common/disable',
label: t('account_model:forbid_channel'),
onClick: () =>
updateChannelStatus(
item.id,
ChannelStatusEnum.ChannelStatusDisabled
)
}
]
: [
{
icon: 'common/enable',
label: t('account_model:enable_channel'),
onClick: () =>
updateChannelStatus(
item.id,
ChannelStatusEnum.ChannelStatusEnabled
)
}
]),
{
icon: 'common/settingLight',
label: t('account_model:edit'),
onClick: () => setEditChannel(item)
},
{
type: 'danger',
icon: 'delete',
label: t('common:common.Delete'),
onClick: () => onDeleteChannel(item.id)
}
]
}
]}
Button={<MyIconButton icon={'more'} />}
/>
</Td>
</Tr>
);
})}
</Tbody>
</Table>
</TableContainer>
</MyBox>
{!!editChannel && (
<EditChannelModal
defaultConfig={editChannel}
onClose={() => setEditChannel(undefined)}
onSuccess={refreshChannelList}
/>
)}
{!!testModels && <ModelTest models={testModels} onClose={() => setTestModels(undefined)} />}
</>
);
};
export default ChannelTable;

View File

@@ -0,0 +1,406 @@
import { getChannelList, getChannelLog, getLogDetail } from '@/web/core/ai/channel';
import { getSystemModelList } from '@/web/core/ai/config';
import { useUserStore } from '@/web/support/user/useUserStore';
import {
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Box,
Flex,
Button,
HStack,
ModalBody,
Grid,
GridItem,
BoxProps
} from '@chakra-ui/react';
import { getModelProvider } from '@fastgpt/global/core/ai/provider';
import DateRangePicker, { DateRangeType } from '@fastgpt/web/components/common/DateRangePicker';
import MyBox from '@fastgpt/web/components/common/MyBox';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import { addDays } from 'date-fns';
import { useTranslation } from 'next-i18next';
import React, { useCallback, useMemo, useState } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time';
import MyModal from '@fastgpt/web/components/common/MyModal';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
type LogDetailType = {
id: number;
request_id: string;
channelName: string | number;
model: React.JSX.Element;
duration: number;
request_at: string;
code: number;
prompt_tokens: number;
completion_tokens: number;
endpoint: string;
content?: string;
request_body?: string;
response_body?: string;
};
const ChannelLog = ({ Tab }: { Tab: React.ReactNode }) => {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const isRoot = userInfo?.username === 'root';
const [filterProps, setFilterProps] = useState<{
channelId?: string;
model?: string;
code_type: 'all' | 'success' | 'error';
dateRange: DateRangeType;
}>({
code_type: 'all',
dateRange: {
from: (() => {
const today = addDays(new Date(), -1);
today.setHours(0, 0, 0, 0);
return today;
})(),
to: (() => {
const today = new Date();
today.setHours(23, 59, 59, 999);
return today;
})()
}
});
const { data: channelList = [] } = useRequest2(
async () => {
const res = await getChannelList().then((res) =>
res.map((item) => ({
label: item.name,
value: `${item.id}`
}))
);
return [
{
label: t('common:common.All'),
value: ''
},
...res
];
},
{
manual: false
}
);
const { data: systemModelList = [] } = useRequest2(getSystemModelList, {
manual: false
});
const modelList = useMemo(() => {
const res = systemModelList
.map((item) => {
const provider = getModelProvider(item.provider);
return {
order: provider.order,
icon: provider.avatar,
label: item.model,
value: item.model
};
})
.sort((a, b) => a.order - b.order);
return [
{
label: t('common:common.All'),
value: ''
},
...res
];
}, [systemModelList]);
const { data, isLoading, ScrollData } = useScrollPagination(getChannelLog, {
pageSize: 20,
refreshDeps: [filterProps],
params: {
channel: filterProps.channelId,
model_name: filterProps.model,
code_type: filterProps.code_type,
start_timestamp: filterProps.dateRange.from?.getTime() || 0,
end_timestamp: filterProps.dateRange.to?.getTime() || 0
}
});
const formatData = useMemo<LogDetailType[]>(() => {
return data.map((item) => {
const duration = item.created_at - item.request_at;
const durationSecond = duration / 1000;
const channelName = channelList.find((channel) => channel.value === `${item.channel}`)?.label;
const model = systemModelList.find((model) => model.model === item.model);
const provider = getModelProvider(model?.provider);
return {
id: item.id,
channelName: channelName || item.channel,
model: (
<HStack>
<MyIcon name={provider?.avatar as any} w={'1rem'} />
<Box>{model?.model}</Box>
</HStack>
),
duration: durationSecond,
request_at: formatTime2YMDHMS(item.request_at),
code: item.code,
prompt_tokens: item.prompt_tokens,
completion_tokens: item.completion_tokens,
request_id: item.request_id,
endpoint: item.endpoint,
content: item.content
};
});
}, [data]);
const [logDetail, setLogDetail] = useState<LogDetailType>();
return (
<>
{isRoot && (
<Flex alignItems={'center'}>
{Tab}
<Box flex={1} />
</Flex>
)}
<HStack spacing={4}>
<HStack>
<FormLabel>{t('common:user.Time')}</FormLabel>
<Box>
<DateRangePicker
defaultDate={filterProps.dateRange}
dateRange={filterProps.dateRange}
position="bottom"
onSuccess={(e) => setFilterProps({ ...filterProps, dateRange: e })}
/>
</Box>
</HStack>
<HStack flex={'0 0 200px'}>
<FormLabel>{t('account_model:channel_name')}</FormLabel>
<Box flex={'1 0 0'}>
<MySelect<string>
bg={'myGray.50'}
isSearch
list={channelList}
placeholder={t('account_model:select_channel')}
value={filterProps.channelId}
onchange={(val) => setFilterProps({ ...filterProps, channelId: val })}
/>
</Box>
</HStack>
<HStack flex={'0 0 200px'}>
<FormLabel>{t('account_model:model_name')}</FormLabel>
<Box flex={'1 0 0'}>
<MySelect<string>
bg={'myGray.50'}
isSearch
list={modelList}
placeholder={t('account_model:select_model')}
value={filterProps.model}
onchange={(val) => setFilterProps({ ...filterProps, model: val })}
/>
</Box>
</HStack>
<HStack flex={'0 0 200px'}>
<FormLabel>{t('account_model:log_status')}</FormLabel>
<Box flex={'1 0 0'}>
<MySelect<'all' | 'success' | 'error'>
bg={'myGray.50'}
list={[
{ label: t('common:common.All'), value: 'all' },
{ label: t('common:common.Success'), value: 'success' },
{ label: t('common:common.failed'), value: 'error' }
]}
value={filterProps.code_type}
onchange={(val) => setFilterProps({ ...filterProps, code_type: val })}
/>
</Box>
</HStack>
</HStack>
<MyBox flex={'1 0 0'} h={0} isLoading={isLoading}>
<ScrollData h={'100%'}>
<TableContainer fontSize={'sm'}>
<Table>
<Thead>
<Tr>
<Th>{t('account_model:channel_name')}</Th>
<Th>{t('account_model:model')}</Th>
<Th>{t('account_model:model_tokens')}</Th>
<Th>{t('account_model:duration')}</Th>
<Th>{t('account_model:channel_status')}</Th>
<Th>{t('account_model:request_at')}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{formatData.map((item) => (
<Tr key={item.id}>
<Td>{item.channelName}</Td>
<Td>{item.model}</Td>
<Td>
{item.prompt_tokens} / {item.completion_tokens}
</Td>
<Td color={item.duration > 10 ? 'red.600' : ''}>{item.duration.toFixed(2)}s</Td>
<Td color={item.code === 200 ? 'green.600' : 'red.600'}>
{item.code}
{item.content && <QuestionTip label={item.content} />}
</Td>
<Td>{item.request_at}</Td>
<Td>
<Button
leftIcon={<MyIcon name={'menu'} w={'1rem'} />}
size={'sm'}
variant={'outline'}
onClick={() => setLogDetail(item)}
>
{t('account_model:detail')}
</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</ScrollData>
</MyBox>
{!!logDetail && <LogDetail data={logDetail} onClose={() => setLogDetail(undefined)} />}
</>
);
};
export default ChannelLog;
const LogDetail = ({ data, onClose }: { data: LogDetailType; onClose: () => void }) => {
const { t } = useTranslation();
const { data: detailData } = useRequest2(
async () => {
if (data.code === 200) return data;
const res = await getLogDetail(data.id);
return {
...res,
...data
};
},
{
manual: false
}
);
const Title = useCallback(({ children, ...props }: { children: React.ReactNode } & BoxProps) => {
return (
<Box
bg={'myGray.50'}
color="myGray.900 "
borderRight={'base'}
p={3}
flex={'0 0 100px'}
{...props}
>
{children}
</Box>
);
}, []);
const Container = useCallback(
({ children, ...props }: { children: React.ReactNode } & BoxProps) => {
return (
<Box p={3} flex={1} {...props}>
{children}
</Box>
);
},
[]
);
return (
<MyModal
isOpen
iconSrc="support/bill/payRecordLight"
title={t('account_model:log_detail')}
onClose={onClose}
maxW={['90vw', '800px']}
w={'100%'}
>
{detailData && (
<ModalBody>
{/* 基本信息表格 */}
<Grid
templateColumns="repeat(2, 1fr)"
gap={0}
borderWidth="1px"
borderRadius="md"
fontSize={'sm'}
overflow={'hidden'}
>
{/* 第一行 */}
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
<Title>RequestID</Title>
<Container>{detailData?.request_id}</Container>
</GridItem>
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
<Title>{t('account_model:channel_status')}</Title>
<Container color={detailData.code === 200 ? 'green.600' : 'red.600'}>
{detailData?.code}
</Container>
</GridItem>
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
<Title>Endpoint</Title>
<Container>{detailData?.endpoint}</Container>
</GridItem>
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
<Title>{t('account_model:channel_name')}</Title>
<Container>{detailData?.channelName}</Container>
</GridItem>
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
<Title>{t('account_model:request_at')}</Title>
<Container>{detailData?.request_at}</Container>
</GridItem>
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
<Title>{t('account_model:duration')}</Title>
<Container>{detailData?.duration.toFixed(2)}s</Container>
</GridItem>
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
<Title>{t('account_model:model')}</Title>
<Container>{detailData?.model}</Container>
</GridItem>
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px">
<Title flex={'0 0 150px'}>{t('account_model:model_tokens')}</Title>
<Container>
{detailData?.prompt_tokens} / {detailData?.completion_tokens}
</Container>
</GridItem>
{detailData?.content && (
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px" colSpan={2}>
<Title>Content</Title>
<Container>{detailData?.content}</Container>
</GridItem>
)}
{detailData?.request_body && (
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px" colSpan={2}>
<Title>Request Body</Title>
<Container userSelect={'all'}>{detailData?.request_body}</Container>
</GridItem>
)}
{detailData?.response_body && (
<GridItem display={'flex'} borderBottomWidth="1px" borderRightWidth="1px" colSpan={2}>
<Title>Response Body</Title>
<Container>{detailData?.response_body}</Container>
</GridItem>
)}
</Grid>
</ModalBody>
)}
</MyModal>
);
};

View File

@@ -33,7 +33,6 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import {
deleteSystemModel,
getModelConfigJson,
getSystemModelDefaultConfig,
getSystemModelDetail,
getSystemModelList,
getTestModel,
@@ -44,24 +43,20 @@ import MyBox from '@fastgpt/web/components/common/MyBox';
import { SystemModelItemType } from '@fastgpt/service/core/ai/type';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import { useForm } from 'react-hook-form';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
import MyTextarea from '@/components/common/Textarea/MyTextarea';
import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
import { clientInitData } from '@/web/common/system/staticData';
import { useUserStore } from '@/web/support/user/useUserStore';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { putUpdateWithJson } from '@/web/core/ai/config';
import CopyBox from '@fastgpt/web/components/common/String/CopyBox';
import MyIcon from '@fastgpt/web/components/common/Icon';
import AIModelSelector from '@/components/Select/AIModelSelector';
import { useRefresh } from '../../../../../../packages/web/hooks/useRefresh';
import { Prompt_CQJson, Prompt_ExtractJson } from '@fastgpt/global/core/ai/prompt/agent';
import MyDivider from '@fastgpt/web/components/common/MyDivider';
import { AddModelButton } from './AddModelBox';
const MyModal = dynamic(() => import('@fastgpt/web/components/common/MyModal'));
const ModelEditModal = dynamic(() => import('./AddModelBox').then((mod) => mod.ModelEditModal));
const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => {
const { t } = useTranslation();
@@ -271,6 +266,7 @@ const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => {
}
}
);
const onCreateModel = (type: ModelTypeEnum) => {
const defaultModel = defaultModels[type];
@@ -316,37 +312,7 @@ const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => {
<Button variant={'whiteBase'} mr={2} onClick={onOpenJsonConfig}>
{t('account:model.json_config')}
</Button>
<MyMenu
trigger="hover"
size="sm"
Button={<Button>{t('account:create_model')}</Button>}
menuList={[
{
children: [
{
label: t('common:model.type.chat'),
onClick: () => onCreateModel(ModelTypeEnum.llm)
},
{
label: t('common:model.type.embedding'),
onClick: () => onCreateModel(ModelTypeEnum.embedding)
},
{
label: t('common:model.type.tts'),
onClick: () => onCreateModel(ModelTypeEnum.tts)
},
{
label: t('common:model.type.stt'),
onClick: () => onCreateModel(ModelTypeEnum.stt)
},
{
label: t('common:model.type.reRank'),
onClick: () => onCreateModel(ModelTypeEnum.rerank)
}
]
}
]}
/>
<AddModelButton onCreate={onCreateModel} />
</Flex>
)}
<MyBox flex={'1 0 0'} isLoading={isLoading}>
@@ -512,650 +478,6 @@ const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => {
);
};
const InputStyles = {
maxW: '300px',
bg: 'myGray.50',
w: '100%',
rows: 3
};
const ModelEditModal = ({
modelData,
onSuccess,
onClose
}: {
modelData: SystemModelItemType;
onSuccess: () => void;
onClose: () => void;
}) => {
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const { register, getValues, setValue, handleSubmit, watch, reset } =
useForm<SystemModelItemType>({
defaultValues: modelData
});
const isCustom = !!modelData.isCustom;
const isLLMModel = modelData?.type === ModelTypeEnum.llm;
const isEmbeddingModel = modelData?.type === ModelTypeEnum.embedding;
const isTTSModel = modelData?.type === ModelTypeEnum.tts;
const isSTTModel = modelData?.type === ModelTypeEnum.stt;
const isRerankModel = modelData?.type === ModelTypeEnum.rerank;
const provider = watch('provider');
const providerData = useMemo(() => getModelProvider(provider), [provider]);
const providerList = useRef<{ label: any; value: ModelProviderIdType }[]>(
ModelProviderList.map((item) => ({
label: (
<HStack>
<Avatar src={item.avatar} w={'1rem'} />
<Box>{t(item.name as any)}</Box>
</HStack>
),
value: item.id
}))
);
const priceUnit = useMemo(() => {
if (isLLMModel || isEmbeddingModel) return '/ 1k Tokens';
if (isTTSModel) return `/ 1k ${t('common:unit.character')}`;
if (isSTTModel) return `/ 60 ${t('common:unit.seconds')}`;
return '';
return '';
}, [isLLMModel, isEmbeddingModel, isTTSModel, t, isSTTModel]);
const { runAsync: updateModel, loading: updatingModel } = useRequest2(
async (data: SystemModelItemType) => {
return putSystemModel({
model: data.model,
metadata: data
}).then(onSuccess);
},
{
onSuccess: () => {
onClose();
},
successToast: t('common:common.Success')
}
);
const [key, setKey] = useState(0);
const { runAsync: loadDefaultConfig, loading: loadingDefaultConfig } = useRequest2(
getSystemModelDefaultConfig,
{
onSuccess(res) {
reset({
...getValues(),
...res
});
setTimeout(() => {
setKey((prev) => prev + 1);
}, 0);
}
}
);
return (
<MyModal
iconSrc={'modal/edit'}
title={t('account:model.edit_model')}
isOpen
onClose={onClose}
maxW={['90vw', '80vw']}
w={'100%'}
h={'100%'}
>
<ModalBody>
<Flex gap={4} key={key}>
<TableContainer flex={'1'}>
<Table>
<Thead>
<Tr color={'myGray.600'}>
<Th fontSize={'xs'}>{t('account:model.param_name')}</Th>
<Th fontSize={'xs'}></Th>
</Tr>
</Thead>
<Tbody>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.model_id')}</Box>
<QuestionTip label={t('account:model.model_id_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
{isCustom ? (
<Input {...register('model', { required: true })} {...InputStyles} />
) : (
modelData?.model
)}
</Td>
</Tr>
<Tr>
<Td>{t('common:model.provider')}</Td>
<Td textAlign={'right'}>
<MySelect
value={provider}
onchange={(value) => setValue('provider', value)}
list={providerList.current}
{...InputStyles}
/>
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.alias')}</Box>
<QuestionTip label={t('account:model.alias_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Input {...register('name', { required: true })} {...InputStyles} />
</Td>
</Tr>
{priceUnit && feConfigs?.isPlus && (
<>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.charsPointsPrice')}</Box>
<QuestionTip label={t('account:model.charsPointsPrice_tip')} />
</HStack>
</Td>
<Td>
<Flex justify="flex-end">
<HStack w={'100%'} maxW={'300px'}>
<MyNumberInput
flex={'1 0 0'}
register={register}
name={'charsPointsPrice'}
step={0.01}
/>
<Box fontSize={'sm'}>{priceUnit}</Box>
</HStack>
</Flex>
</Td>
</Tr>
{isLLMModel && (
<>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.input_price')}</Box>
<QuestionTip label={t('account:model.input_price_tip')} />
</HStack>
</Td>
<Td>
<Flex justify="flex-end">
<HStack w={'100%'} maxW={'300px'}>
<MyNumberInput
flex={'1 0 0'}
register={register}
name={'inputPrice'}
step={0.01}
/>
<Box fontSize={'sm'}>{priceUnit}</Box>
</HStack>
</Flex>
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.output_price')}</Box>
<QuestionTip label={t('account:model.output_price_tip')} />
</HStack>
</Td>
<Td>
<Flex justify="flex-end">
<HStack w={'100%'} maxW={'300px'}>
<MyNumberInput
flex={'1 0 0'}
register={register}
name={'outputPrice'}
step={0.01}
/>
<Box fontSize={'sm'}>{priceUnit}</Box>
</HStack>
</Flex>
</Td>
</Tr>
</>
)}
</>
)}
{isLLMModel && (
<>
<Tr>
<Td>{t('common:core.ai.Max context')}</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<MyNumberInput
register={register}
isRequired
name="maxContext"
{...InputStyles}
/>
</Flex>
</Td>
</Tr>
<Tr>
<Td>{t('account:model.max_quote')}</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<MyNumberInput
register={register}
isRequired
name="quoteMaxToken"
{...InputStyles}
/>
</Flex>
</Td>
</Tr>
<Tr>
<Td>{t('common:core.chat.response.module maxToken')}</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<MyNumberInput
register={register}
isRequired
name="maxResponse"
{...InputStyles}
/>
</Flex>
</Td>
</Tr>
<Tr>
<Td>{t('account:model.max_temperature')}</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<MyNumberInput
register={register}
isRequired
name="maxTemperature"
step={0.1}
{...InputStyles}
/>
</Flex>
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.show_top_p')}</Box>
</HStack>
</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('showTopP')} />
</Flex>
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.show_stop_sign')}</Box>
</HStack>
</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('showStopSign')} />
</Flex>
</Td>
</Tr>
<Tr>
<Td>{t('account:model.response_format')}</Td>
<Td textAlign={'right'}>
<JsonEditor
value={JSON.stringify(getValues('responseFormatList'), null, 2)}
resize
onChange={(e) => {
if (!e) {
setValue('responseFormatList', []);
return;
}
try {
setValue('responseFormatList', JSON.parse(e));
} catch (error) {
console.error(error);
}
}}
{...InputStyles}
/>
</Td>
</Tr>
</>
)}
{isEmbeddingModel && (
<>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.normalization')}</Box>
<QuestionTip label={t('account:model.normalization_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('normalization')} />
</Flex>
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.default_token')}</Box>
<QuestionTip label={t('account:model.default_token_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<MyNumberInput
register={register}
isRequired
name="defaultToken"
{...InputStyles}
/>
</Flex>
</Td>
</Tr>
<Tr>
<Td>{t('common:core.ai.Max context')}</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<MyNumberInput
register={register}
isRequired
name="maxToken"
{...InputStyles}
/>
</Flex>
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.defaultConfig')}</Box>
<QuestionTip label={t('account:model.defaultConfig_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<JsonEditor
value={JSON.stringify(getValues('defaultConfig'), null, 2)}
onChange={(e) => {
if (!e) {
setValue('defaultConfig', {});
return;
}
try {
setValue('defaultConfig', JSON.parse(e));
} catch (error) {
console.error(error);
}
}}
{...InputStyles}
/>
</Flex>
</Td>
</Tr>
</>
)}
{isTTSModel && (
<>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.voices')}</Box>
<QuestionTip label={t('account:model.voices_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<JsonEditor
value={JSON.stringify(getValues('voices'), null, 2)}
onChange={(e) => {
try {
setValue('voices', JSON.parse(e));
} catch (error) {
console.error(error);
}
}}
{...InputStyles}
/>
</Flex>
</Td>
</Tr>
</>
)}
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.request_url')}</Box>
<QuestionTip label={t('account:model.request_url_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Input {...register('requestUrl')} {...InputStyles} />
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.request_auth')}</Box>
<QuestionTip label={t('account:model.request_auth_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Input {...register('requestAuth')} {...InputStyles} />
</Td>
</Tr>
</Tbody>
</Table>
</TableContainer>
{isLLMModel && (
<TableContainer flex={'1'}>
<Table>
<Thead>
<Tr color={'myGray.600'}>
<Th fontSize={'xs'}>{t('account:model.param_name')}</Th>
<Th fontSize={'xs'}></Th>
</Tr>
</Thead>
<Tbody>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.tool_choice')}</Box>
<QuestionTip label={t('account:model.tool_choice_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('toolChoice')} />
</Flex>
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.function_call')}</Box>
<QuestionTip label={t('account:model.function_call_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('functionCall')} />
</Flex>
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.vision')}</Box>
<QuestionTip label={t('account:model.vision_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('vision')} />
</Flex>
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.reasoning')}</Box>
<QuestionTip label={t('account:model.reasoning_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('reasoning')} />
</Flex>
</Td>
</Tr>
{feConfigs?.isPlus && (
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.censor')}</Box>
<QuestionTip label={t('account:model.censor_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('censor')} />
</Flex>
</Td>
</Tr>
)}
<Tr>
<Td>{t('account:model.dataset_process')}</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('datasetProcess')} />
</Flex>
</Td>
</Tr>
<Tr>
<Td>{t('account:model.used_in_classify')}</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('usedInClassify')} />
</Flex>
</Td>
</Tr>
<Tr>
<Td>{t('account:model.used_in_extract_fields')}</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('usedInExtractFields')} />
</Flex>
</Td>
</Tr>
<Tr>
<Td>{t('account:model.used_in_tool_call')}</Td>
<Td textAlign={'right'}>
<Flex justifyContent={'flex-end'}>
<Switch {...register('usedInToolCall')} />
</Flex>
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.default_system_chat_prompt')}</Box>
<QuestionTip label={t('account:model.default_system_chat_prompt_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<MyTextarea {...register('defaultSystemChatPrompt')} {...InputStyles} />
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.custom_cq_prompt')}</Box>
<QuestionTip
label={t('account:model.custom_cq_prompt_tip', { prompt: Prompt_CQJson })}
/>
</HStack>
</Td>
<Td textAlign={'right'}>
<MyTextarea {...register('customCQPrompt')} {...InputStyles} />
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.custom_extract_prompt')}</Box>
<QuestionTip
label={t('account:model.custom_extract_prompt_tip', {
prompt: Prompt_ExtractJson
})}
/>
</HStack>
</Td>
<Td textAlign={'right'}>
<MyTextarea {...register('customExtractPrompt')} {...InputStyles} />
</Td>
</Tr>
<Tr>
<Td>
<HStack spacing={1}>
<Box>{t('account:model.default_config')}</Box>
<QuestionTip label={t('account:model.default_config_tip')} />
</HStack>
</Td>
<Td textAlign={'right'}>
<JsonEditor
value={JSON.stringify(getValues('defaultConfig'), null, 2)}
resize
onChange={(e) => {
if (!e) {
setValue('defaultConfig', {});
return;
}
try {
setValue('defaultConfig', JSON.parse(e));
} catch (error) {
console.error(error);
}
}}
{...InputStyles}
/>
</Td>
</Tr>
</Tbody>
</Table>
</TableContainer>
)}
</Flex>
</ModalBody>
<ModalFooter>
{!modelData.isCustom && (
<Button
isLoading={loadingDefaultConfig}
variant={'whiteBase'}
mr={4}
onClick={() => loadDefaultConfig(modelData.model)}
>
{t('account:reset_default')}
</Button>
)}
<Button variant={'whiteBase'} mr={4} onClick={onClose}>
{t('common:common.Cancel')}
</Button>
<Button isLoading={updatingModel} onClick={handleSubmit(updateModel)}>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
};
const JsonConfigModal = ({
onClose,
onSuccess

View File

@@ -7,13 +7,17 @@ import { useUserStore } from '@/web/support/user/useUserStore';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import { useSystemStore } from '@/web/common/system/useSystemStore';
const ModelConfigTable = dynamic(() => import('@/pageComponents/account/model/ModelConfigTable'));
const ChannelTable = dynamic(() => import('@/pageComponents/account/model/Channel'));
const ChannelLog = dynamic(() => import('@/pageComponents/account/model/Log'));
type TabType = 'model' | 'config' | 'channel';
type TabType = 'model' | 'config' | 'channel' | 'channel_log';
const ModelProvider = () => {
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const [tab, setTab] = useState<TabType>('model');
@@ -22,21 +26,29 @@ const ModelProvider = () => {
<FillRowTabs<TabType>
list={[
{ label: t('account:active_model'), value: 'model' },
{ label: t('account:config_model'), value: 'config' }
// { label: t('account:channel'), value: 'channel' }
{ label: t('account:config_model'), value: 'config' },
// @ts-ignore
...(feConfigs?.show_aiproxy
? [
{ label: t('account:channel'), value: 'channel' },
{ label: t('account_model:log'), value: 'channel_log' }
]
: [])
]}
value={tab}
py={1}
onChange={setTab}
/>
);
}, [t, tab]);
}, [feConfigs.show_aiproxy, t, tab]);
return (
<AccountContainer>
<Flex h={'100%'} flexDirection={'column'} gap={4} py={4} px={6}>
{tab === 'model' && <ValidModelTable Tab={Tab} />}
{tab === 'config' && <ModelConfigTable Tab={Tab} />}
{tab === 'channel' && <ChannelTable Tab={Tab} />}
{tab === 'channel_log' && <ChannelLog Tab={Tab} />}
</Flex>
</AccountContainer>
);
@@ -45,7 +57,7 @@ const ModelProvider = () => {
export async function getServerSideProps(content: any) {
return {
props: {
...(await serviceSideProps(content, ['account']))
...(await serviceSideProps(content, ['account', 'account_model']))
}
};
}

View File

@@ -0,0 +1,72 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { request } from 'https';
import { authSystemAdmin } from '@fastgpt/service/support/permission/user/auth';
const baseUrl = process.env.AIPROXY_API_ENDPOINT;
const token = process.env.AIPROXY_API_TOKEN;
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await authSystemAdmin({ req });
if (!baseUrl || !token) {
throw new Error('AIPROXY_API_ENDPOINT or AIPROXY_API_TOKEN is not set');
}
const { path = [], ...query } = req.query as any;
if (!path.length) {
throw new Error('url is empty');
}
const queryStr = new URLSearchParams(query).toString();
const requestPath = queryStr
? `/${path?.join('/')}?${new URLSearchParams(query).toString()}`
: `/${path?.join('/')}`;
const parsedUrl = new URL(baseUrl);
delete req.headers?.cookie;
delete req.headers?.host;
delete req.headers?.origin;
const requestResult = request({
protocol: parsedUrl.protocol,
hostname: parsedUrl.hostname,
port: parsedUrl.port,
path: requestPath,
method: req.method,
headers: {
...req.headers,
Authorization: `Bearer ${token}`
},
timeout: 30000
});
req.pipe(requestResult);
requestResult.on('response', (response) => {
Object.keys(response.headers).forEach((key) => {
// @ts-ignore
res.setHeader(key, response.headers[key]);
});
response.statusCode && res.writeHead(response.statusCode);
response.pipe(res);
});
requestResult.on('error', (e) => {
res.send(e);
res.end();
});
} catch (error) {
jsonRes(res, {
code: 500,
error
});
}
}
export const config = {
api: {
bodyParser: false
}
};

View File

@@ -0,0 +1,33 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { authSystemAdmin } from '@fastgpt/service/support/permission/user/auth';
import axios from 'axios';
import { getErrText } from '@fastgpt/global/common/error/utils';
const baseUrl = process.env.AIPROXY_API_ENDPOINT;
const token = process.env.AIPROXY_API_TOKEN;
async function handler(req: ApiRequestProps, res: ApiResponseType<any>) {
try {
await authSystemAdmin({ req });
if (!baseUrl || !token) {
return Promise.reject('AIPROXY_API_ENDPOINT or AIPROXY_API_TOKEN is not set');
}
const { data } = await axios.post(`${baseUrl}/api/channel/`, req.body, {
headers: {
Authorization: `Bearer ${token}`
}
});
res.json(data);
} catch (error) {
res.json({
success: false,
message: getErrText(error),
data: error
});
}
}
export default handler;

View File

@@ -60,6 +60,7 @@ const testLLMModel = async (model: LLMModelItemType) => {
const ai = getAIApi({
timeout: 10000
});
const requestBody = llmCompletionsBodyFormat(
{
model: model.model,

View File

@@ -218,7 +218,7 @@ const MyApps = () => {
size="md"
Button={
<Button variant={'primary'} leftIcon={<AddIcon />}>
<Box>{t('common:common.Create New')}</Box>
<Box>{t('common:new_create')}</Box>
</Button>
}
menuList={[

View File

@@ -147,7 +147,7 @@ const Dataset = () => {
<Button variant={'primary'} px="0">
<Flex alignItems={'center'} px={5}>
<AddIcon mr={2} />
<Box>{t('common:common.Create New')}</Box>
<Box>{t('common:new_create')}</Box>
</Flex>
</Button>
}

View File

@@ -83,7 +83,8 @@ export async function initSystemConfig() {
...fileRes?.feConfigs,
...defaultFeConfigs,
...(dbConfig.feConfigs || {}),
isPlus: !!FastGPTProUrl
isPlus: !!FastGPTProUrl,
show_aiproxy: !!process.env.AIPROXY_API_ENDPOINT
},
systemEnv: {
...fileRes.systemEnv,

View File

@@ -0,0 +1,183 @@
import axios, { Method, AxiosResponse } from 'axios';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
import {
ChannelInfoType,
ChannelListResponseType,
ChannelLogListItemType,
CreateChannelProps
} from '@/global/aiproxy/type';
import { ChannelStatusEnum } from '@/global/aiproxy/constants';
interface ResponseDataType {
success: boolean;
message: string;
data: any;
}
/**
* 请求成功,检查请求头
*/
function responseSuccess(response: AxiosResponse<ResponseDataType>) {
return response;
}
/**
* 响应数据检查
*/
function checkRes(data: ResponseDataType) {
if (data === undefined) {
console.log('error->', data, 'data is empty');
return Promise.reject('服务器异常');
} else if (!data.success) {
return Promise.reject(data);
}
return data.data;
}
/**
* 响应错误
*/
function responseError(err: any) {
console.log('error->', '请求错误', err);
const data = err?.response?.data || err;
if (!err) {
return Promise.reject({ message: '未知错误' });
}
if (typeof err === 'string') {
return Promise.reject({ message: err });
}
if (typeof data === 'string') {
return Promise.reject(data);
}
return Promise.reject(data);
}
/* 创建请求实例 */
const instance = axios.create({
timeout: 60000, // 超时时间
headers: {
'content-type': 'application/json'
}
});
/* 响应拦截 */
instance.interceptors.response.use(responseSuccess, (err) => Promise.reject(err));
function request(url: string, data: any, method: Method): any {
/* 去空 */
for (const key in data) {
if (data[key] === undefined) {
delete data[key];
}
}
return instance
.request({
baseURL: getWebReqUrl('/api/aiproxy/api'),
url,
method,
data: ['POST', 'PUT'].includes(method) ? data : undefined,
params: !['POST', 'PUT'].includes(method) ? data : undefined
})
.then((res) => checkRes(res.data))
.catch((err) => responseError(err));
}
/**
* api请求方式
* @param {String} url
* @param {Any} params
* @param {Object} config
* @returns
*/
export function GET<T = undefined>(url: string, params = {}): Promise<T> {
return request(url, params, 'GET');
}
export function POST<T = undefined>(url: string, data = {}): Promise<T> {
return request(url, data, 'POST');
}
export function PUT<T = undefined>(url: string, data = {}): Promise<T> {
return request(url, data, 'PUT');
}
export function DELETE<T = undefined>(url: string, data = {}): Promise<T> {
return request(url, data, 'DELETE');
}
// ====== API ======
export const getChannelList = () =>
GET<ChannelListResponseType>('/channels/all', {
page: 1,
perPage: 10
});
export const getChannelProviders = () =>
GET<
Record<
number,
{
defaultBaseUrl: string;
keyHelp: string;
name: string;
}
>
>('/channels/type_metas');
export const postCreateChannel = (data: CreateChannelProps) =>
POST(`/createChannel`, {
type: data.type,
name: data.name,
base_url: data.base_url,
models: data.models,
model_mapping: data.model_mapping,
key: data.key
});
export const putChannelStatus = (id: number, status: ChannelStatusEnum) =>
POST(`/channel/${id}/status`, {
status
});
export const putChannel = (data: ChannelInfoType) =>
PUT(`/channel/${data.id}`, {
type: data.type,
name: data.name,
base_url: data.base_url,
models: data.models,
model_mapping: data.model_mapping,
key: data.key,
status: data.status,
priority: data.priority
});
export const deleteChannel = (id: number) => DELETE(`/channel/${id}`);
export const getChannelLog = (params: {
channel?: string;
model_name?: string;
status?: 'all' | 'success' | 'error';
start_timestamp: number;
end_timestamp: number;
offset: number;
pageSize: number;
}) =>
GET<{
logs: ChannelLogListItemType[];
total: number;
}>(`/logs/search`, {
...params,
p: Math.floor(params.offset / params.pageSize) + 1,
per_page: params.pageSize,
offset: undefined,
pageSize: undefined
}).then((res) => {
return {
list: res.logs,
total: res.total
};
});
export const getLogDetail = (id: number) =>
GET<{
request_body: string;
response_body: string;
}>(`/logs/detail/${id}`);