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

@@ -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