mirror of
https://github.com/APIParkLab/APIPark.git
synced 2026-06-04 10:13:53 +08:00
feat: load banancing list
This commit is contained in:
@@ -160,6 +160,13 @@ const mockData = [
|
||||
path: '/aiApis',
|
||||
icon: 'ic:baseline-api',
|
||||
access: 'system.settings.ai_api.view'
|
||||
},
|
||||
{
|
||||
name: '负载均衡',
|
||||
key: 'loadBalancing',
|
||||
path: '/loadBalancing',
|
||||
icon: 'ph:network-x',
|
||||
access: 'system.settings.data_source.view'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -219,6 +219,16 @@ const mockData = {
|
||||
type: 'normal'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
driver: 'apipark.builtIn.component',
|
||||
name: 'loadBalancing',
|
||||
router: [
|
||||
{
|
||||
path: 'loadBalancing',
|
||||
type: 'normal'
|
||||
}
|
||||
]
|
||||
}
|
||||
// {
|
||||
// "driver": "apipark.remote.normal",
|
||||
|
||||
@@ -799,5 +799,20 @@ export const routerMap: Map<string, RouterMapConfig> = new Map([
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
'loadBalancing',
|
||||
{
|
||||
type: 'module',
|
||||
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/loadBalancing/loadBalancingLayout.tsx')),
|
||||
key: 'loadBalancing',
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/loadBalancing/index.tsx')),
|
||||
key: 'loadBalancingList'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
@@ -79,7 +79,7 @@ const AiServiceInsideRouterCreate = () => {
|
||||
timeout,
|
||||
retry,
|
||||
aiPrompt: { variables: variables, prompt: prompt },
|
||||
aiModel: { id: defaultLlm?.id, provider: defaultLlm?.provider, config: defaultLlm?.config },
|
||||
aiModel: { id: defaultLlm?.id, provider: defaultLlm?.provider, config: defaultLlm?.config, type: defaultLlm?.type },
|
||||
disabled
|
||||
}
|
||||
return fetchData<BasicResponse<null>>('service/ai-router', {
|
||||
@@ -237,13 +237,14 @@ const AiServiceInsideRouterCreate = () => {
|
||||
}
|
||||
|
||||
const handlerSubmit: () => Promise<boolean> | undefined = () => {
|
||||
return drawerAddFormRef.current?.save()?.then((res: { id: string; config: string }) => {
|
||||
return drawerAddFormRef.current?.save()?.then((res: { id: string; config: string, type: string, provider: string }) => {
|
||||
setDefaultLlm(
|
||||
(prev) =>
|
||||
({
|
||||
...prev,
|
||||
provider: res.provider,
|
||||
id: res.id,
|
||||
type: res.type,
|
||||
config: res.config,
|
||||
logo: llmList?.find((x: AiProviderLlmsItems) => x.id === res.id)?.logo
|
||||
}) as AiProviderDefaultConfig & { config: string }
|
||||
|
||||
+198
-113
@@ -1,132 +1,217 @@
|
||||
import { Codebox } from "@common/components/postcat/api/Codebox"
|
||||
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const"
|
||||
import { useFetch } from "@common/hooks/http"
|
||||
import { $t } from "@common/locales"
|
||||
import { AiProviderDefaultConfig, AiProviderLlmsItems } from "@core/pages/aiSetting/AiSettingList"
|
||||
import { SimpleAiProviderItem } from "@core/pages/system/SystemConfig"
|
||||
import { Form, message, Select, Tag } from "antd"
|
||||
import { DefaultOptionType } from "antd/es/select"
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react"
|
||||
import { Codebox } from '@common/components/postcat/api/Codebox'
|
||||
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import { AiProviderDefaultConfig, AiProviderLlmsItems } from '@core/pages/aiSetting/AiSettingList'
|
||||
import { LocalLlmType } from '@core/pages/loadBalancing/type'
|
||||
import { SimpleAiProviderItem } from '@core/pages/system/SystemConfig'
|
||||
import { Form, message, Select, Tag } from 'antd'
|
||||
import { DefaultOptionType } from 'antd/es/select'
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
|
||||
|
||||
export type AiServiceRouterModelConfigHandle = {
|
||||
save:()=>Promise<{id:string, config:string}>
|
||||
save: () => Promise<{ id: string; config: string, type: string, provider: string }>
|
||||
}
|
||||
|
||||
export type AiServiceRouterModelConfigProps = {
|
||||
entity:AiServiceRouterModelConfigField
|
||||
llmList:AiProviderLlmsItems[]
|
||||
entity: AiServiceRouterModelConfigField
|
||||
llmList: AiProviderLlmsItems[]
|
||||
}
|
||||
|
||||
type AiServiceRouterModelConfigField = {
|
||||
provider:string
|
||||
id:string
|
||||
config:string
|
||||
provider: string
|
||||
id: string
|
||||
config: string
|
||||
type: string
|
||||
}
|
||||
|
||||
const AiServiceRouterModelConfig = forwardRef<AiServiceRouterModelConfigHandle, AiServiceRouterModelConfigProps>((props, ref)=>{
|
||||
const [form] = Form.useForm();
|
||||
const {entity} = props
|
||||
const [providerList, setProviderList]= useState<DefaultOptionType[]>([])
|
||||
const [llmList, setLlmList]= useState<DefaultOptionType[]>([])
|
||||
const {fetchData} = useFetch()
|
||||
useImperativeHandle(ref, ()=>({
|
||||
save:form.validateFields
|
||||
})
|
||||
)
|
||||
const AiServiceRouterModelConfig = forwardRef<AiServiceRouterModelConfigHandle, AiServiceRouterModelConfigProps>(
|
||||
(props, ref) => {
|
||||
const [form] = Form.useForm()
|
||||
const { entity } = props
|
||||
const [providerList, setProviderList] = useState<DefaultOptionType[]>([])
|
||||
const [llmList, setLlmList] = useState<DefaultOptionType[]>([])
|
||||
const [modelType, setModelType] = useState<'online' | 'local'>('online')
|
||||
const { fetchData } = useFetch()
|
||||
useImperativeHandle(ref, () => ({
|
||||
save: form.validateFields
|
||||
}))
|
||||
const [modelTypeList] = useState([
|
||||
{
|
||||
label: $t('线上模型'),
|
||||
value: 'online'
|
||||
},
|
||||
{
|
||||
label: $t('本地模型'),
|
||||
value: 'local'
|
||||
}
|
||||
])
|
||||
|
||||
useEffect(()=>{
|
||||
/**
|
||||
* 获取本地模型列表
|
||||
* @param setDefaultValue
|
||||
*/
|
||||
const getLocalLlmList = (setDefaultValue?: boolean) => {
|
||||
fetchData<LocalLlmType[]>('simple/ai/models/local/configured', {
|
||||
method: 'GET',
|
||||
eoTransformKeys: ['default_config']
|
||||
}).then((response) => {
|
||||
const models = response.data.models || []
|
||||
setLlmList(models)
|
||||
if (setDefaultValue && models.length) {
|
||||
const id = models[0].id
|
||||
form.setFieldsValue({
|
||||
id,
|
||||
config: models.find((x) => x.id === id)?.defaultConfig
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换模型类型
|
||||
* @param e
|
||||
*/
|
||||
const modelTypeChange = (e: string) => {
|
||||
setModelType(e as 'online' | 'local')
|
||||
setLlmList([])
|
||||
form.setFieldsValue({
|
||||
provider: '',
|
||||
id: '',
|
||||
config: '',
|
||||
type: e
|
||||
})
|
||||
if (e === 'online') {
|
||||
getProviderList(true)
|
||||
} else {
|
||||
getLocalLlmList(true)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (entity.type === 'online') {
|
||||
getProviderList()
|
||||
form.setFieldsValue(entity)
|
||||
},[])
|
||||
|
||||
const getProviderList = ()=>{
|
||||
setProviderList([])
|
||||
fetchData<BasicResponse<{ providers: SimpleAiProviderItem[] }>>('simple/ai/providers',{method:'GET',eoTransformKeys:[]}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
setProviderList(data.providers?.filter(x=>x.configured)?.map((x:SimpleAiProviderItem)=>{return {...x,
|
||||
label: x.name, value:x.id
|
||||
}}))
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getLlmList = (provider:string)=>{
|
||||
fetchData<BasicResponse<{llms:AiProviderLlmsItems[],provider:AiProviderDefaultConfig}>>('ai/provider/llms',{method:'GET',eoParams:{provider}, eoTransformKeys:['default_llm']}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
setLlmList(data.llms)
|
||||
form.setFieldsValue({
|
||||
id:data.provider.defaultLlm,
|
||||
config:data.llms.find(x=>x.id===data.provider.defaultLlm)?.config})
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}).catch((errorInfo)=> console.error(errorInfo))
|
||||
}
|
||||
|
||||
const handleChangeProvider = (provider:string)=>{
|
||||
getLlmList(provider)
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
getLlmList(entity.provider)
|
||||
},[])
|
||||
} else {
|
||||
getLocalLlmList()
|
||||
}
|
||||
form.setFieldsValue(entity)
|
||||
}, [])
|
||||
|
||||
const getProviderList = (setDefaultValue?: boolean) => {
|
||||
setProviderList([])
|
||||
fetchData<BasicResponse<{ providers: SimpleAiProviderItem[] }>>('simple/ai/providers/configured', {
|
||||
method: 'GET',
|
||||
eoTransformKeys: []
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setProviderList(
|
||||
data.providers
|
||||
?.map((x: SimpleAiProviderItem) => {
|
||||
return { ...x, label: x.name, value: x.id }
|
||||
})
|
||||
)
|
||||
if (setDefaultValue && data.providers.length) {
|
||||
const id = data.providers[0].id
|
||||
form.setFieldValue('provider', id)
|
||||
getLlmList(id)
|
||||
}
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getLlmList = (provider: string) => {
|
||||
fetchData<BasicResponse<{ llms: AiProviderLlmsItems[]; provider: AiProviderDefaultConfig }>>('ai/provider/llms', {
|
||||
method: 'GET',
|
||||
eoParams: { provider },
|
||||
eoTransformKeys: ['default_llm']
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setLlmList(data.llms)
|
||||
form.setFieldsValue({
|
||||
id: data.provider.defaultLlm,
|
||||
config: data.llms.find((x) => x.id === data.provider.defaultLlm)?.config
|
||||
})
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => console.error(errorInfo))
|
||||
}
|
||||
|
||||
const handleChangeProvider = (provider: string) => {
|
||||
getLlmList(provider)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
layout='vertical'
|
||||
labelAlign='left'
|
||||
scrollToFirstError
|
||||
form={form}
|
||||
className="mx-auto flex flex-col h-full"
|
||||
name="aiServiceInsideRouterModalConfig"
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item<AiServiceRouterModelConfigField>
|
||||
label={$t("模型供应商")}
|
||||
name="provider"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={providerList}
|
||||
onChange={(e)=>{
|
||||
handleChangeProvider(e)
|
||||
}}>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form
|
||||
layout="vertical"
|
||||
labelAlign="left"
|
||||
scrollToFirstError
|
||||
form={form}
|
||||
className="mx-auto flex flex-col h-full"
|
||||
name="aiServiceInsideRouterModalConfig"
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item<AiServiceRouterModelConfigField> label={$t('模型类型')} name="type" rules={[{ required: true }]}>
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={modelTypeList}
|
||||
onChange={(e) => {
|
||||
modelTypeChange(e)
|
||||
}}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
{modelType === 'online' && (
|
||||
<Form.Item<AiServiceRouterModelConfigField>
|
||||
label={$t('模型供应商')}
|
||||
name="provider"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={providerList}
|
||||
onChange={(e) => {
|
||||
handleChangeProvider(e)
|
||||
}}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item<AiServiceRouterModelConfigField>
|
||||
label={$t("模型")}
|
||||
name="id"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={llmList?.map(x=>({
|
||||
value:x.id,
|
||||
label:<div className="flex items-center gap-[10px]">
|
||||
<span>{x.id}</span>
|
||||
{x?.scopes?.map(s=><Tag >{s?.toLocaleUpperCase()}</Tag>)}
|
||||
</div>}))}
|
||||
onChange={(e)=>{
|
||||
form.setFieldValue('config',llmList.find(x=>x.id===e)?.config)
|
||||
}}>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item<AiServiceRouterModelConfigField> label={$t('模型')} name="id" rules={[{ required: true }]}>
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={
|
||||
llmList?.map((x) => ({
|
||||
value: x.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-[10px]" key={x.id}>
|
||||
<span>{x.id}</span>
|
||||
{modelType === 'online' && x?.scopes?.map((s: any) => <Tag>{s?.toLocaleUpperCase()}</Tag>)}
|
||||
</div>
|
||||
)
|
||||
}))
|
||||
}
|
||||
onChange={(e) => {
|
||||
form.setFieldValue('config', llmList.find((x) => x.id === e)?.config || llmList.find((x) => x.id === e)?.defaultConfig)
|
||||
}}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<AiServiceRouterModelConfigField>
|
||||
label={$t("参数")}
|
||||
name="config"
|
||||
>
|
||||
<Codebox editorTheme="vs-dark"
|
||||
width="100%" height="300px" language='json' enableToolbar={false} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Form.Item<AiServiceRouterModelConfigField> label={$t('参数')} name="config">
|
||||
<Codebox editorTheme="vs-dark" width="100%" height="300px" language="json" enableToolbar={false} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export default AiServiceRouterModelConfig
|
||||
export default AiServiceRouterModelConfig
|
||||
|
||||
@@ -272,7 +272,7 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
||||
disabled={readOnly}
|
||||
>
|
||||
{modelMode === 'manual' && (
|
||||
<Form.Item<ModelDetailData> label={$t('模型来源')} name="modelMode" rules={[{ required: true }]}>
|
||||
<Form.Item<ModelDetailData> label={$t('模型供应商')} name="modelMode" rules={[{ required: true }]}>
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
|
||||
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { App, Form, Select, Tag } from 'antd'
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
|
||||
import { LoadBalancingHandle, LoadModelDetailData, LocalLlmType } from './type'
|
||||
import { ApiResponse } from '../aiSetting/AIFlowChart'
|
||||
import { AiProviderLlmsItems, ModelListData } from '../aiSetting/types'
|
||||
import { DefaultOptionType } from 'antd/es/select'
|
||||
const AddLoadBalancingModel = forwardRef<LoadBalancingHandle>((props, ref: any) => {
|
||||
const [form] = Form.useForm()
|
||||
const [modelProviderLoading, setModelProviderLoading] = useState(false)
|
||||
const [modelProviderData, setModelProviderData] = useState<ModelListData[]>([])
|
||||
const [llmList, setLlmList] = useState<DefaultOptionType[]>()
|
||||
const [modelType, setModelType] = useState<'online' | 'local'>('online')
|
||||
const { message } = App.useApp()
|
||||
const [llmListLoading, setLlmListLoading] = useState<boolean>(false)
|
||||
const { fetchData } = useFetch()
|
||||
|
||||
const [modelTypeList] = useState([
|
||||
{
|
||||
label: $t('线上模型'),
|
||||
value: 'online'
|
||||
},
|
||||
{
|
||||
label: $t('本地模型'),
|
||||
value: 'local'
|
||||
}
|
||||
])
|
||||
|
||||
/**
|
||||
* 获取 llm 列表
|
||||
* @param id
|
||||
*/
|
||||
const getLlmList = (id?: string) => {
|
||||
setLlmListLoading(true)
|
||||
fetchData<BasicResponse<{ llms: AiProviderLlmsItems[] }>>(`ai/provider/llms`, {
|
||||
method: 'GET',
|
||||
eoParams: { provider: id },
|
||||
eoTransformKeys: ['default_llm']
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setLlmList(data.llms)
|
||||
form.setFieldValue('model', data.provider?.defaultLlm)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setLlmListLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置表单数据
|
||||
* @param e
|
||||
*/
|
||||
const resetFormData = (e = 'online') => {
|
||||
form.setFieldValue('type', e)
|
||||
form.setFieldValue('model', '')
|
||||
form.setFieldValue('provider', '')
|
||||
setModelProviderData([])
|
||||
setLlmList([])
|
||||
setModelType(e as 'online' | 'local')
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换模型类型
|
||||
* @param e
|
||||
*/
|
||||
const modelTypeChange = (e: string) => {
|
||||
resetFormData(e)
|
||||
if (e === 'online') {
|
||||
setModelProviderLoading(true)
|
||||
fetchData<ApiResponse>('simple/ai/providers/configured', {
|
||||
method: 'GET',
|
||||
eoTransformKeys: ['default_llm']
|
||||
})
|
||||
.then((response) => {
|
||||
const mockApiResponse: ApiResponse = response as ApiResponse
|
||||
const providers = mockApiResponse.data.providers || []
|
||||
setModelProviderData(providers)
|
||||
if (providers.length) {
|
||||
const id = providers[0].id
|
||||
form.setFieldValue('provider', id)
|
||||
getLlmList(id)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setModelProviderLoading(false)
|
||||
})
|
||||
} else {
|
||||
setLlmListLoading(true)
|
||||
fetchData<LocalLlmType[]>('simple/ai/models/local/configured', {
|
||||
method: 'GET'
|
||||
})
|
||||
.then((response) => {
|
||||
const models = response.data.models || []
|
||||
setLlmList(models)
|
||||
if (models.length) {
|
||||
const id = models[0].id
|
||||
form.setFieldValue('model', id)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setLlmListLoading(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型提供商变化
|
||||
* @param e
|
||||
*/
|
||||
const modelProviderChange = (e: string) => {
|
||||
form.setFieldValue('modelProvider', e)
|
||||
getLlmList(e)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
modelTypeChange('online')
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
const save = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
fetchData<ApiResponse>('ai/balance', {
|
||||
method: 'POST',
|
||||
eoBody: values
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
resolve(true)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
reject(errorInfo)
|
||||
})
|
||||
})
|
||||
}
|
||||
useImperativeHandle(ref, () => ({
|
||||
save
|
||||
}))
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
labelAlign="left"
|
||||
scrollToFirstError
|
||||
className="flex flex-col mx-auto h-full"
|
||||
name="aiServiceInsideRouterModalConfig"
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item<LoadModelDetailData> label={$t('模型类型')} name="type" rules={[{ required: true }]}>
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={modelTypeList}
|
||||
onChange={(e) => {
|
||||
modelTypeChange(e)
|
||||
}}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
{modelType === 'online' && (
|
||||
<Form.Item<LoadModelDetailData> label={$t('模型供应商')} name="provider" rules={[{ required: true }]}>
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
loading={modelProviderLoading}
|
||||
options={modelProviderData?.map((x) => ({
|
||||
value: x.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<span>{x.name}</span>
|
||||
</div>
|
||||
)
|
||||
}))}
|
||||
onChange={(e) => {
|
||||
modelProviderChange(e)
|
||||
}}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item label={$t('模型')} name="model" className="mt-[16px]" rules={[{ required: true }]}>
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.input)}
|
||||
loading={llmListLoading}
|
||||
options={
|
||||
llmList?.map((x) => ({
|
||||
value: x.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-[10px]">
|
||||
<span>{x.id}</span>
|
||||
{ modelType === 'online' &&x?.scopes?.map((s: any) => <Tag key={s}>{s?.toLocaleUpperCase()}</Tag>)}
|
||||
</div>
|
||||
)
|
||||
}))
|
||||
}
|
||||
onChange={(value) => {
|
||||
form.setFieldValue('model', value)
|
||||
}}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
})
|
||||
|
||||
export default AddLoadBalancingModel
|
||||
@@ -0,0 +1,349 @@
|
||||
import { ActionType } from '@ant-design/pro-components'
|
||||
import InsidePage from '@common/components/aoplatform/InsidePage'
|
||||
import PageList, { PageProColumns } from '@common/components/aoplatform/PageList'
|
||||
import WithPermission from '@common/components/aoplatform/WithPermission'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { App, Button, Typography } from 'antd'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { LoadBalancingHandle, LoadBalancingItems } from './type'
|
||||
import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission'
|
||||
import AddLoadBalancingModel from './AddModel'
|
||||
|
||||
|
||||
|
||||
const LoadBalancingPage = () => {
|
||||
const pageListRef = useRef<ActionType>(null)
|
||||
const [searchParams] = useSearchParams()
|
||||
const serviceId = searchParams.get('serviceId')
|
||||
const [searchWord, setSearchWord] = useState<string>('')
|
||||
const [columns, setColumns] = useState<PageProColumns<LoadBalancingItems>[]>([])
|
||||
const { modal, message } = App.useApp()
|
||||
const [apiKeys, setApiKeys] = useState<LoadBalancingItems[]>([])
|
||||
const addModelRef = useRef<LoadBalancingHandle>()
|
||||
const statusEnum: Record<string, { text: React.ReactNode }> = {
|
||||
normal: { text: <Typography.Text type="success">{$t('正常')}</Typography.Text> },
|
||||
abnormal: { text: <Typography.Text type="danger">{$t('异常')}</Typography.Text> }
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求数据
|
||||
*/
|
||||
const { fetchData } = useFetch()
|
||||
const addModel = () => {
|
||||
modal.confirm({
|
||||
title: $t('添加负载均衡'),
|
||||
content: <AddLoadBalancingModel ref={addModelRef} />,
|
||||
width: 600,
|
||||
closable: true,
|
||||
onOk: () => {
|
||||
return addModelRef.current?.save().then((res) => {
|
||||
if (res === true) {
|
||||
pageListRef.current?.reload()
|
||||
}
|
||||
})
|
||||
},
|
||||
wrapClassName: 'ant-modal-without-footer',
|
||||
okText: $t('确认'),
|
||||
cancelText: $t('取消'),
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取列表数据
|
||||
* @param dataType
|
||||
* @returns
|
||||
*/
|
||||
const requestApis = (
|
||||
params: LoadBalancingItems & {
|
||||
pageSize: number
|
||||
current: number
|
||||
},
|
||||
sort: Record<string, string>,
|
||||
filter: Record<string, string>
|
||||
) => {
|
||||
let filters
|
||||
if (filter) {
|
||||
filters = []
|
||||
if (filter.isStop) {
|
||||
if (filter.isStop.indexOf('true') !== -1) {
|
||||
filters.push('enable')
|
||||
}
|
||||
if (filter.isStop.indexOf('false') !== -1) {
|
||||
filters.push('disable')
|
||||
}
|
||||
if (filter.publishStatus?.length > 0) {
|
||||
filters = [...filters, ...filter.publishStatus]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fetchData<BasicResponse<{ list: LoadBalancingItems[]; total: number }>>(
|
||||
`strategy/${serviceId === undefined ? 'global' : 'service'}/data-masking/list`,
|
||||
{
|
||||
method: 'GET',
|
||||
eoParams: {
|
||||
order: Object.keys(sort)?.[0],
|
||||
sort: Object.keys(sort)?.length > 0 ? (Object.values(sort)?.[0] === 'descend' ? 'desc' : 'asc') : undefined,
|
||||
filters: JSON.stringify(filters),
|
||||
keyword: searchWord,
|
||||
service: serviceId
|
||||
},
|
||||
eoTransformKeys: ['is_stop', 'is_delete', 'update_time', 'publish_status', 'processed_total']
|
||||
}
|
||||
)
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setApiKeys(response.data.list)
|
||||
// 保存数据
|
||||
return {
|
||||
data: data.list,
|
||||
total: data.total,
|
||||
success: true
|
||||
}
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return { data: [], success: false }
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
return { data: [], success: false }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 排序
|
||||
* @param beforeIndex
|
||||
* @param afterIndex
|
||||
* @param newDataSource
|
||||
*/
|
||||
const handleDragSortEnd = async (beforeIndex: number, afterIndex: number, newDataSource: LoadBalancingItems[]) => {
|
||||
try {
|
||||
let targetId
|
||||
let sortDirection
|
||||
|
||||
// Check if there's an item before afterIndex
|
||||
if (afterIndex > 0) {
|
||||
targetId = newDataSource[afterIndex - 1].id
|
||||
sortDirection = 'after'
|
||||
} else if (afterIndex < newDataSource.length - 1) {
|
||||
// If no item before, use the item after
|
||||
targetId = newDataSource[afterIndex + 1].id
|
||||
sortDirection = 'before'
|
||||
}
|
||||
|
||||
const response = await fetchData<BasicResponse<any>>('ai/balance/sort', {
|
||||
method: 'PUT',
|
||||
eoBody: {
|
||||
origin: apiKeys[beforeIndex].id,
|
||||
target: targetId,
|
||||
sort: sortDirection
|
||||
}
|
||||
})
|
||||
|
||||
if (response.code === STATUS_CODE.SUCCESS) {
|
||||
message.success($t('排序成功'))
|
||||
pageListRef.current?.reload()
|
||||
} else {
|
||||
message.error(response.msg || RESPONSE_TIPS.error)
|
||||
// Revert the UI if API call fails
|
||||
pageListRef.current?.reload()
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(RESPONSE_TIPS.error)
|
||||
// Revert the UI if API call fails
|
||||
pageListRef.current?.reload()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
* @param id
|
||||
*/
|
||||
const handleDelete = (id: string) => {
|
||||
fetchData<BasicResponse<null>>('ai/balance', {
|
||||
method: 'DELETE',
|
||||
eoBody: {
|
||||
id
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
const { code } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success($t('删除成功'))
|
||||
pageListRef.current?.reload()
|
||||
} else {
|
||||
message.error(RESPONSE_TIPS.error)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
message.error(RESPONSE_TIPS.error)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置表格列
|
||||
*/
|
||||
const setTableColumns = () => {
|
||||
setColumns([
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'drag',
|
||||
width: '40px'
|
||||
},
|
||||
{
|
||||
title: $t('优先级'),
|
||||
dataIndex: 'priority',
|
||||
width: 80,
|
||||
ellipsis: true,
|
||||
key: 'priority'
|
||||
},
|
||||
{
|
||||
title: $t('模型'),
|
||||
dataIndex: ['provider', 'name'],
|
||||
ellipsis: true,
|
||||
width: 100,
|
||||
key: 'provider',
|
||||
render: (text: string, record: LoadBalancingItems) => (
|
||||
<span>
|
||||
{record.provider?.name} / {record.model?.name}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: $t('类型'),
|
||||
dataIndex: 'type',
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
key: 'type',
|
||||
render: (text: string, record: LoadBalancingItems) => (
|
||||
<span>{record.type === 'online' ? $t('线上模型') : $t('本地模型')}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: $t('状态'),
|
||||
dataIndex: 'state',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
key: 'state',
|
||||
render: (text: string, record: LoadBalancingItems) => <span>{statusEnum[record.state]?.text || '-'}</span>
|
||||
},
|
||||
{
|
||||
title: $t('API 数量'),
|
||||
dataIndex: 'api_count',
|
||||
ellipsis: true,
|
||||
width: 80,
|
||||
key: 'api_count',
|
||||
render: (text: string, record: LoadBalancingItems) => (
|
||||
<span className="[&>.key-link]:text-[#2196f3] cursor-pointer">
|
||||
<a
|
||||
href={`/aiApis?modelId=${record.model?.id}`}
|
||||
target="_blank"
|
||||
className="key-link"
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
cursor: 'pointer',
|
||||
pointerEvents: 'all',
|
||||
textDecoration: 'none'
|
||||
}}
|
||||
>
|
||||
{record.api_count || '-'}
|
||||
</a>
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: $t('KEY 数量'),
|
||||
dataIndex: 'key_count',
|
||||
ellipsis: true,
|
||||
width: 80,
|
||||
key: 'key_count',
|
||||
render: (text: string, record: LoadBalancingItems) => (
|
||||
<span className="[&>.key-link]:text-[#2196f3] cursor-pointer">
|
||||
<a
|
||||
href={`/keysetting?modelId=${record.model?.id}`}
|
||||
target="_blank"
|
||||
className="key-link"
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
cursor: 'pointer',
|
||||
pointerEvents: 'all',
|
||||
textDecoration: 'none'
|
||||
}}
|
||||
>
|
||||
{record.key_count || '-'}
|
||||
</a>
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
key: 'option',
|
||||
btnNums: 1,
|
||||
width: 80,
|
||||
fixed: 'right',
|
||||
valueType: 'option',
|
||||
render: (_: React.ReactNode, entity: any) => [
|
||||
<TableBtnWithPermission
|
||||
access="system.settings.ai_key_resource.manager"
|
||||
key="delete"
|
||||
btnType="delete"
|
||||
onClick={() => handleDelete(entity.id as string)}
|
||||
btnTitle={$t('删除')}
|
||||
/>
|
||||
]
|
||||
}
|
||||
])
|
||||
}
|
||||
useEffect(() => {
|
||||
setTableColumns()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<InsidePage
|
||||
pageTitle={$t('负载均衡')}
|
||||
description={$t(
|
||||
'系统自动识别异常AI模型后,自动替换成以下优先级最高的可用模型。这将确保您的AI应用保持高可用性和最佳性能,从而防止任何单个LLM异常成为您的性能瓶颈。'
|
||||
)}
|
||||
showBorder={false}
|
||||
scrollPage={false}
|
||||
>
|
||||
<div className="h-[calc(100%-1rem-36px)] pr-PAGE_INSIDE_X">
|
||||
<PageList
|
||||
ref={pageListRef}
|
||||
rowKey="id"
|
||||
afterNewBtn={[
|
||||
<WithPermission key="removeFromDepPermission" access={``}>
|
||||
<Button className="mr-btnbase" type="primary" key="removeFromDep" onClick={() => addModel()}>
|
||||
{$t('添加模型')}
|
||||
</Button>
|
||||
</WithPermission>
|
||||
]}
|
||||
request={async (
|
||||
params: any & {
|
||||
pageSize: number
|
||||
current: number
|
||||
},
|
||||
sort: Record<string, string>,
|
||||
filter: Record<string, string>
|
||||
) => requestApis(params, sort, filter)}
|
||||
onSearchWordChange={(e) => {
|
||||
setSearchWord(e.target.value)
|
||||
}}
|
||||
showPagination={true}
|
||||
dragSortKey="drag"
|
||||
onDragSortEnd={handleDragSortEnd}
|
||||
searchPlaceholder={$t('请输入...')}
|
||||
columns={columns}
|
||||
/>
|
||||
</div>
|
||||
</InsidePage>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default LoadBalancingPage
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useEffect } from 'react'
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
export default function LoadBalancingLayout() {
|
||||
const location = useLocation()
|
||||
const pathName = location.pathname
|
||||
const navigator = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
if (pathName === '/loadBalancing') {
|
||||
const queryParams = new URLSearchParams(location.search).toString()
|
||||
navigator(`/loadBalancing/list${queryParams ? `?${queryParams}` : ''}`)
|
||||
}
|
||||
}, [pathName])
|
||||
return <Outlet></Outlet>
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
export interface LoadBalancingItems {
|
||||
id: string
|
||||
priority: string
|
||||
provider: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
model: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
type: string
|
||||
state: string
|
||||
api_count: string
|
||||
key_count: string
|
||||
}
|
||||
|
||||
export interface LoadModelDetailData {
|
||||
type: string
|
||||
provider: string
|
||||
model: string
|
||||
}
|
||||
export interface LocalLlmType {
|
||||
id: string
|
||||
name: string
|
||||
defaultConfig: string
|
||||
}
|
||||
|
||||
export type LoadBalancingHandle = {
|
||||
save: () => Promise<boolean | string>
|
||||
}
|
||||
Reference in New Issue
Block a user