From e108ba20babfc447687b1b862aec53ded67d34c3 Mon Sep 17 00:00:00 2001 From: xpf <965732851@qq.com> Date: Fri, 8 Nov 2024 17:52:25 +0800 Subject: [PATCH 01/17] =?UTF-8?q?feat:=20AI=E8=B7=AF=E7=94=B1=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=B7=BB=E5=8A=A0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/interfaces/service-source.ts | 10 + frontend/src/locales/zh-CN/translation.json | 16 +- .../ai/components/ProviderForm/index.tsx | 321 ++++++++++++-- .../ai/components/RouteForm/Components.tsx | 119 +++++ .../pages/ai/components/RouteForm/index.tsx | 410 ++++++++++++++++-- frontend/src/pages/ai/provider.tsx | 25 +- frontend/src/pages/ai/route.tsx | 20 +- 7 files changed, 831 insertions(+), 90 deletions(-) create mode 100644 frontend/src/pages/ai/components/RouteForm/Components.tsx diff --git a/frontend/src/interfaces/service-source.ts b/frontend/src/interfaces/service-source.ts index bcf76a8a..1a3c0eac 100644 --- a/frontend/src/interfaces/service-source.ts +++ b/frontend/src/interfaces/service-source.ts @@ -49,3 +49,13 @@ export const ServiceSourceTypes = { static: { key: 'static', name: 'serviceSource.types.static.name', i18n: true, enabled: true }, dns: { key: 'dns', name: 'serviceSource.types.dns.name', i18n: true, enabled: true }, }; + +interface ListEntry { + provider: string; + width: number; +} + +export interface CustomComponentHandles { + addItem: (any) => void; + getList: () => Array; +}; \ No newline at end of file diff --git a/frontend/src/locales/zh-CN/translation.json b/frontend/src/locales/zh-CN/translation.json index 458c2870..1c459c6d 100644 --- a/frontend/src/locales/zh-CN/translation.json +++ b/frontend/src/locales/zh-CN/translation.json @@ -141,9 +141,23 @@ "qwen": "通义千问", "moonshot": "月之暗面" }, + "providerForm": { + "label": { + "type": "类型", + "name": "名称" + }, + "placeholder": { + "type": "请选择类型", + "name": "请输入名称", + "tokens": "请输入认证令牌" + }, + "rules": { + "name": "支持大小写字母、数字和短划线,并以字母或数字开头,且不以短划线(-)结尾。长度不超过63个字符。" + } + }, "create": "提供AI服务提供者", "edit": "编辑AI服务提供者", - "deleteConfirmation": "确定删除 <1>{{currentConsumerName}} 吗?" + "deleteConfirmation": "确定删除 <1>{{currentLlmProviderName}} 吗?" }, "plugins": { "title": "策略配置", diff --git a/frontend/src/pages/ai/components/ProviderForm/index.tsx b/frontend/src/pages/ai/components/ProviderForm/index.tsx index d66d47ca..13f4ab71 100644 --- a/frontend/src/pages/ai/components/ProviderForm/index.tsx +++ b/frontend/src/pages/ai/components/ProviderForm/index.tsx @@ -1,16 +1,62 @@ import { ServiceSourceTypes } from '@/interfaces/service-source'; -import { Form, Input, Select, Tabs } from 'antd'; +import { Form, Input, Select, Tabs, Button, Switch, InputNumber } from 'antd'; import TextArea from 'antd/lib/input/TextArea'; import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; const { Option } = Select; +const providerTypeDisplayName = [ + { key: 'openai', label:'llmProvider.providerTypes.openai' }, + { key: 'qwen', label:'llmProvider.providerTypes.qwen' }, +]; + const ProviderForm: React.FC = forwardRef((props, ref) => { const { t } = useTranslation(); - const { value } = props; const [form] = Form.useForm(); + const { value } = props; + const [form] = Form.useForm(); + const [enabled, setEnabled] = useState(false); + useEffect(() => { form.resetFields(); + if(value) { + const { + name, + type, + protocol, + tokens, + modelMapping={}, + tokenFailoverConfig={} + } = value; + const { + enabled=false, + failureThreshold, + successThreshold, + healthCheckInterval, + healthCheckTimeout, + healthCheckModel + } = tokenFailoverConfig ?? {}; + + setEnabled(enabled); + form.setFieldsValue({ + name, + type, + protocol, + tokens, + modelMapping: getModelText(modelMapping), + enabled, + failureThreshold, + successThreshold, + healthCheckInterval, + healthCheckTimeout, + healthCheckModel + }); + }; + + return () => { + setEnabled(false); + } }, [value]); useImperativeHandle(ref, () => ({ @@ -19,59 +65,270 @@ const ProviderForm: React.FC = forwardRef((props, ref) => { }, handleSubmit: async () => { const values = await form.validateFields(); - return values; + + const result = { + type: values.type, + name: values.name, + tokens: values.tokens, + version: 0, // 资源版本号。进行创建或强制更新操作时需设置为 0。 / 1 表示强制更新 + protocol: values.protocol, + modelMapping: getModelMapping(values.modelMapping), + tokenFailoverConfig: { + enabled: values.enabled, + } + }; + + if(values.enabled) { + result.tokenFailoverConfig['failureThreshold'] = values.failureThreshold; + result.tokenFailoverConfig['successThreshold'] = values.successThreshold; + result.tokenFailoverConfig['healthCheckInterval'] = values.healthCheckInterval; + result.tokenFailoverConfig['healthCheckTimeout'] = values.healthCheckTimeout; + result.tokenFailoverConfig['healthCheckModel'] = values.healthCheckModel; + }; + + return result; }, })); + + const getModelMapping = (text) => { + try { + const lines = text.split('\n'); + const result = {}; + + lines.forEach(line => { + const [key, value] = line.split('='); + result[key.trim()] = value.trim(); + }); + + return result; + } catch (err) { + return {}; + } + }; + + const getModelText = (text) => { + try { + return Object.entries(text).map(([key, value]) => `${key}=${value}`).join('\n'); + } catch (err) { + return JSON.stringify(err); + } + }; + return (
+ {/* 类型 */} + + + + {/* 名称 */} + - - ), - }, - { - label: 'OAuth2', - key: 'oauth2', - children: ( - <> - ), - }, - { - label: 'JWT', - key: 'jwt-auth', - children: ( - <> - ), - }, - ]} - /> + + {/* 类型 */} + + + + + {/* 认证令牌 */} + + {(fields, { add, remove }, { errors }) => ( + <> + {fields.length? null: +
+ {t('llmProvider.columns.tokens')} +
+ } + + {fields.map((field, index) => ( + + + + + {/* 删除按钮 */} + {fields.length > 1 ? + ( + ) +}; + +const UpstreamsTable = React.forwardRef((props, ref) => { + const [dataSource, setDataSource] = useState([]); + + useImperativeHandle(ref, () => ({ + addItem: (item) => { + setDataSource([ ...dataSource, item ].map(i => ({...i, provider: i.name, weight: 100 }))); + }, + getList: () => { + return dataSource.map(({ provider, weight }) => ({ provider, weight })); + } + })); + + const columns = [ + { + title: '服务', + dataIndex: 'name', + key: 'name', + }, + { + title: '权重', + dataIndex: 'weight', + key: 'weight', + render: (_, record) => ( + weightChange(val, record)} + /> + ) + }, + { + title: '操作', + dataIndex: 'operation', + key: 'operation', + render: (_, record) => ( + + ), + }, + ]; + + const deleteData = (record) => { + const newList = dataSource.filter(i => i.name!== record.name); + setDataSource(newList); + }; + + const weightChange = (val, record) => { + const newList = dataSource.map(i => { + if(i.name === record.name) { + return { + ...i, + weight: val + } + } + return i; + }); + + setDataSource(newList); + }; + + return ( + <> + + + ); +}); + +export { + RedoOutlinedBtn, + HistoryButton, + UpstreamsTable, +}; \ No newline at end of file diff --git a/frontend/src/pages/ai/components/RouteForm/index.tsx b/frontend/src/pages/ai/components/RouteForm/index.tsx index 6d44b4f4..30a599d9 100644 --- a/frontend/src/pages/ai/components/RouteForm/index.tsx +++ b/frontend/src/pages/ai/components/RouteForm/index.tsx @@ -1,17 +1,70 @@ -import { ServiceSourceTypes } from '@/interfaces/service-source'; -import { Form, Input, Select, Tabs } from 'antd'; -import TextArea from 'antd/lib/input/TextArea'; -import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; +import { ServiceSourceTypes, CustomComponentHandles } from '@/interfaces/service-source'; +import { Form, Input, Select, Tabs, Switch, Button } from 'antd'; +import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; - -const { Option } = Select; +import { RedoOutlined, PlusOutlined } from '@ant-design/icons'; +import { LlmProvider } from '@/interfaces/llm-provider'; +import { getLlmProviders, } from '@/services/llm-provider'; +import { Domain } from '@/interfaces/domain'; +import { getConsumers } from '@/services/consumer'; +import { Consumer } from '@/interfaces/consumer'; +import { useRequest } from 'ahooks'; +import { getGatewayDomains } from '@/services'; +import { RedoOutlinedBtn, HistoryButton, UpstreamsTable } from './Components'; const ConsumerForm: React.FC = forwardRef((props, ref) => { const { t } = useTranslation(); - const { value } = props; const [form] = Form.useForm(); + const { value } = props; + const [form] = Form.useForm(); + const [ modelPredicate_enabled, setModelPredicateEnabled] = useState(false); + const [ fallbackConfig_enabled, setFallbackConfigEnabled] = useState(false); + const [ authConfig_enabled, setAuthConfigEnabled] = useState(false); + // 目标AI服务错误提示 + const [upstreamsError, setUpstreamsError] = useState(false); + const upstreamsRef = useRef(null); + + const [llmList, setLlmList] = useState([]); + const llmResult = useRequest(getLlmProviders, { + manual: true, + onSuccess: (result) => { + const llmProviders = (result || []) as LlmProvider[]; + llmProviders.sort((i1, i2) => { + return i1.name.localeCompare(i2.name); + }) + setLlmList(llmProviders); + }, + }); + + const [consumerList, setConsumerList] = useState([]); + const consumerResult = useRequest(getConsumers, { + manual: true, + onSuccess: (result) => { + const consumers = (result || []) as Consumer[]; + setConsumerList(consumers); + }, + }); + + const [domainsList, setDomainsList] = useState([]); + const domainsResult = useRequest(getGatewayDomains, { + manual: true, + onSuccess: (result) => { + const consumers = (result || []) as Domain[]; + setDomainsList(consumers); + }, + }); + useEffect(() => { + llmResult.run(); + consumerResult.run(); + domainsResult.run(); form.resetFields(); - }, [value]); + + return () => { + setAuthConfigEnabled(false); + setFallbackConfigEnabled(false); + setModelPredicateEnabled(false); + } + }, []); useImperativeHandle(ref, () => ({ reset: () => { @@ -19,7 +72,58 @@ const ConsumerForm: React.FC = forwardRef((props, ref) => { }, handleSubmit: async () => { const values = await form.validateFields(); - return values; + console.log('values', values) + + const upstreams = upstreamsRef.current?.getList(); + + if(!upstreams?.length) { + setUpstreamsError(true); + return false; + }; + + const { + name='', + domains=[], + modelPredicate_enabled=false, + modelPredicate_prefix='', + fallbackConfig_upstreams=[], + fallbackConfig_strategy='', + authConfig_allowedConsumers=[] + } = values; + + const payload = { + name, + domains, + + modelPredicate: { + enabled: modelPredicate_enabled, + }, + + upstreams: upstreams, + + fallbackConfig: { + enabled: fallbackConfig_enabled, + }, + + authConfig:{ + enabled: authConfig_enabled, + } + }; + + if(modelPredicate_enabled) { + payload['modelPredicate']['prefix'] = modelPredicate_prefix; + }; + + if(fallbackConfig_enabled) { + payload['fallbackConfig']['upstreams'] = fallbackConfig_upstreams; + payload['fallbackConfig']['strategy'] = fallbackConfig_strategy; + }; + + if(authConfig_enabled) { + payload['authConfig']['consuallowedConsumersmer'] = authConfig_allowedConsumers; + }; + + return payload; }, })); @@ -28,50 +132,280 @@ const ConsumerForm: React.FC = forwardRef((props, ref) => { form={form} layout="vertical" > + {/* 名称 */} - - ), - }, - { - label: 'OAuth2', - key: 'oauth2', - children: ( - <> - ), - }, - { - label: 'JWT', - key: 'jwt-auth', - children: ( - <> - ), - }, - ]} - /> + + {/* 域名 */} +
+ )} + > + + + + +
+ + + {/* 是否启用基于模型的路由匹配规则 */} + + setModelPredicateEnabled(e)}/> + + + {/* 匹配本路由所需的模型名称前缀(不包含结尾的“/”) */} + { + modelPredicate_enabled? + + + :null + } + + {/* 目标AI服务 upstreams */} +
+ )} + > + + + + +
+ + + + + {/* 降级服务 */} + + setFallbackConfigEnabled(e)}/> + + + { + fallbackConfig_enabled? + <> + {/* 降级服务列表 */} +
+ + + + + +
+ + {/* 路由降级策略 */} + + + + + :null + } + + {/* 请求认证设置 */} + + setAuthConfigEnabled(e)}/> + + + {/* 允许请求本路由的消费者名称列表 */} + {authConfig_enabled? +
+ )} + > + + + + +
+ :null} ); }); diff --git a/frontend/src/pages/ai/provider.tsx b/frontend/src/pages/ai/provider.tsx index 9e4e14aa..3737b5eb 100644 --- a/frontend/src/pages/ai/provider.tsx +++ b/frontend/src/pages/ai/provider.tsx @@ -34,6 +34,12 @@ const LlmProviderList: React.FC = () => { key: 'name', ellipsis: true, }, + { + title: t('service.columns.endpoints'), + dataIndex: 'endpoints', + key: 'endpoints', + ellipsis: true, + }, { title: t('llmProvider.columns.tokens'), dataIndex: 'tokens', @@ -80,7 +86,7 @@ const LlmProviderList: React.FC = () => { }); useEffect(() => { - run({}); + run(); }, []); const onEditDrawer = (llmProvider: LlmProvider) => { @@ -95,20 +101,19 @@ const LlmProviderList: React.FC = () => { const handleDrawerOK = async () => { try { - // const values: FormProps = formRef.current ? await formRef.current.handleSubmit() : {} as FormProps; + const values = formRef.current ? await formRef.current.handleSubmit() : {}; - // if (currentLlmProvider) { - // await updateLlmProvider({ version: currentLlmProvider.version, ...values } as LlmProvider); - // } else { - // await addLlmProvider(values as LlmProvider); - // } + if (currentLlmProvider) { + // version 进行创建或强制更新操作时需设置为 0。 + await updateLlmProvider({ version: 0, ...values } as LlmProvider); + } else { + await addLlmProvider(values as LlmProvider); + } setOpenDrawer(false); formRef.current && formRef.current.reset(); refresh(); - } catch (errInfo) { - console.log('Save failed: ', errInfo); - } + } catch (errInfo) {} }; const handleDrawerCancel = () => { diff --git a/frontend/src/pages/ai/route.tsx b/frontend/src/pages/ai/route.tsx index e3c72292..bbc26f03 100644 --- a/frontend/src/pages/ai/route.tsx +++ b/frontend/src/pages/ai/route.tsx @@ -1,5 +1,5 @@ import { AiRoute, AiUpstream } from '@/interfaces/ai-route'; -import { deleteAiRoute, getAiRoutes } from '@/services/ai-route'; +import { addAiRoute, deleteAiRoute, getAiRoutes, updateAiRoute } from '@/services/ai-route'; import { ArrowRightOutlined, ExclamationCircleOutlined, RedoOutlined } from '@ant-design/icons'; import { PageContainer } from '@ant-design/pro-layout'; import { useRequest } from 'ahooks'; @@ -109,7 +109,7 @@ const AiRouteList: React.FC = () => { }); useEffect(() => { - run({}); + run(); }, []); const onEditDrawer = (aiRoute: AiRoute) => { @@ -124,13 +124,15 @@ const AiRouteList: React.FC = () => { const handleDrawerOK = async () => { try { - // const values: FormProps = formRef.current ? await formRef.current.handleSubmit() : {} as FormProps; - - // if (currentAiRoute) { - // await updateAiRoute({ version: currentAiRoute.version, ...values } as AiRoute); - // } else { - // await addAiRoute(values as AiRoute); - // } + const values = formRef.current ? await formRef.current.handleSubmit() : {}; + console.log('values', values) + if(!values) return false; + + if (currentAiRoute) { + await updateAiRoute({ version: currentAiRoute.version, ...values } as AiRoute); + } else { + await addAiRoute(values as AiRoute); + } setOpenDrawer(false); formRef.current && formRef.current.reset(); From 908accb2e37d9ffdd11ac3216e0af68743e5e074 Mon Sep 17 00:00:00 2001 From: xpf <965732851@qq.com> Date: Tue, 12 Nov 2024 09:08:25 +0800 Subject: [PATCH 02/17] =?UTF-8?q?feat:=E5=AE=8C=E6=88=90=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/components/RouteForm/Components.tsx | 50 +-- .../pages/ai/components/RouteForm/index.tsx | 345 +++++++++--------- frontend/src/pages/ai/provider.tsx | 18 +- frontend/src/pages/ai/route.tsx | 16 +- 4 files changed, 233 insertions(+), 196 deletions(-) diff --git a/frontend/src/pages/ai/components/RouteForm/Components.tsx b/frontend/src/pages/ai/components/RouteForm/Components.tsx index 47a676c3..cd16ffca 100644 --- a/frontend/src/pages/ai/components/RouteForm/Components.tsx +++ b/frontend/src/pages/ai/components/RouteForm/Components.tsx @@ -14,10 +14,10 @@ const RedoOutlinedBtn = (props) => { return ( - @@ -41,56 +42,57 @@ const UpstreamsTable = React.forwardRef((props, ref) => const [dataSource, setDataSource] = useState([]); useImperativeHandle(ref, () => ({ + setDataSource, addItem: (item) => { - setDataSource([ ...dataSource, item ].map(i => ({...i, provider: i.name, weight: 100 }))); + setDataSource([...dataSource, item].map(i => ({ provider: i.name, weight: 100, ...i }))); }, getList: () => { return dataSource.map(({ provider, weight }) => ({ provider, weight })); - } + }, })); const columns = [ { title: '服务', - dataIndex: 'name', - key: 'name', + dataIndex: 'provider', + key: 'provider', }, { title: '权重', dataIndex: 'weight', key: 'weight', render: (_, record) => ( - weightChange(val, record)} /> - ) + ), }, { title: '操作', dataIndex: 'operation', key: 'operation', render: (_, record) => ( - + ), }, ]; const deleteData = (record) => { - const newList = dataSource.filter(i => i.name!== record.name); + const newList = dataSource.filter(i => i.provider !== record.provider); setDataSource(newList); }; const weightChange = (val, record) => { const newList = dataSource.map(i => { - if(i.name === record.name) { + if (i.provider === record.provider) { return { - ...i, - weight: val + ...i, + weight: val, } } return i; @@ -101,8 +103,8 @@ const UpstreamsTable = React.forwardRef((props, ref) => return ( <> -
{ const { t } = useTranslation(); - const { value } = props; + const { value } = props; const [form] = Form.useForm(); - const [ modelPredicate_enabled, setModelPredicateEnabled] = useState(false); - const [ fallbackConfig_enabled, setFallbackConfigEnabled] = useState(false); - const [ authConfig_enabled, setAuthConfigEnabled] = useState(false); + const [modelPredicate_enabled, setModelPredicateEnabled] = useState(false); + const [fallbackConfig_enabled, setFallbackConfigEnabled] = useState(false); + const [authConfig_enabled, setAuthConfigEnabled] = useState(false); // 目标AI服务错误提示 const [upstreamsError, setUpstreamsError] = useState(false); const upstreamsRef = useRef(null); @@ -59,6 +59,8 @@ const ConsumerForm: React.FC = forwardRef((props, ref) => { domainsResult.run(); form.resetFields(); + if (value) initForm(); + return () => { setAuthConfigEnabled(false); setFallbackConfigEnabled(false); @@ -66,62 +68,88 @@ const ConsumerForm: React.FC = forwardRef((props, ref) => { } }, []); + const initForm = () => { + const { name = "", domains, upstreams = [] } = value; + if (upstreams?.length) { + upstreamsRef.current?.setDataSource(upstreams); + } + + const _modelPredicate_enabled = value?.modelPredicate?.enabled || false; + const _authConfig_enabled = value?.authConfig?.enabled || false; + const _fallbackConfig_enabled = value?.fallbackConfig?.enabled || false; + + setAuthConfigEnabled(_authConfig_enabled); + setFallbackConfigEnabled(_fallbackConfig_enabled); + setModelPredicateEnabled(_modelPredicate_enabled); + + form.setFieldsValue({ + name, + domains: domains?.length ? domains[0] : undefined, + upstreams, + modelPredicate_enabled: _modelPredicate_enabled, + modelPredicate_prefix: value?.modelPredicate?.prefix || "", + authConfig_enabled: _authConfig_enabled, + authConfig_allowedConsumers: value?.authConfig?.allowedConsumers || "", + fallbackConfig_enabled: _fallbackConfig_enabled, + fallbackConfig_upstreams: value?.fallbackConfig?.upstreams ? value?.fallbackConfig?.upstreams[0].provider : undefined, + fallbackConfig_strategy: value?.fallbackConfig?.strategy, + }) + }; + useImperativeHandle(ref, () => ({ reset: () => { form.resetFields(); }, handleSubmit: async () => { const values = await form.validateFields(); - console.log('values', values) - const upstreams = upstreamsRef.current?.getList(); - if(!upstreams?.length) { + if (!upstreams?.length) { setUpstreamsError(true); return false; - }; + } - const { - name='', - domains=[], - modelPredicate_enabled=false, - modelPredicate_prefix='', - fallbackConfig_upstreams=[], - fallbackConfig_strategy='', - authConfig_allowedConsumers=[] - } = values; + const { + name = '', + domains = [], + modelPredicate_prefix = '', + fallbackConfig_upstreams = '', + fallbackConfig_strategy = '', + authConfig_allowedConsumers = '', + } = values; const payload = { name, - domains, + domains: domains ? [domains] : [], + modelPredicate: { - enabled: modelPredicate_enabled, + enabled: values?.modelPredicate_enabled, }, - upstreams: upstreams, + upstreams, fallbackConfig: { enabled: fallbackConfig_enabled, }, - authConfig:{ + authConfig: { enabled: authConfig_enabled, - } + }, }; - if(modelPredicate_enabled) { + if (values?.modelPredicate_enabled) { payload['modelPredicate']['prefix'] = modelPredicate_prefix; - }; + } - if(fallbackConfig_enabled) { - payload['fallbackConfig']['upstreams'] = fallbackConfig_upstreams; + if (fallbackConfig_enabled) { + payload['fallbackConfig']['upstreams'] = fallbackConfig_upstreams ? [{ provider: fallbackConfig_upstreams }] : []; payload['fallbackConfig']['strategy'] = fallbackConfig_strategy; - }; + } - if(authConfig_enabled) { - payload['authConfig']['consuallowedConsumersmer'] = authConfig_allowedConsumers; - }; + if (authConfig_enabled) { + payload['authConfig']['consuallowedConsumersmer'] = authConfig_allowedConsumers ? [authConfig_allowedConsumers] : []; + } return payload; }, @@ -170,13 +198,12 @@ const ConsumerForm: React.FC = forwardRef((props, ref) => { > - + {/* 是否启用基于模型的路由匹配规则 */} - - setModelPredicateEnabled(e)}/> + setModelPredicateEnabled(e)} /> {/* 匹配本路由所需的模型名称前缀(不包含结尾的“/”) */} { - modelPredicate_enabled? - - - :null + modelPredicate_enabled ? + + + : null } {/* 目标AI服务 upstreams */} @@ -230,23 +257,17 @@ const ConsumerForm: React.FC = forwardRef((props, ref) => { label={"目标AI服务"} required hasFeedback={upstreamsError} - validateStatus={upstreamsError? "error":''} - help={upstreamsError? "请选择并添加目标AI服务": null} + validateStatus={upstreamsError ? "error" : ''} + help={upstreamsError ? "请选择并添加目标AI服务" : null} name="upstreams_target" - // rules={[ - // { - // required: true, - // message: "请输入目标AI服务", - // }, - // ]} extra={()} > + { + llmList.map((item) => { + return ( + + {item.name} + + ) + }) + } + + + + + + + {/* 路由降级策略 */} - + 随机选择列表中的一个服务提供商进行降级,只降级一次(默认) + 按服务提供商列表顺序逐个尝试降级,直至请求成功或所有提供商均尝试过为止 + + : null + } - - + {/* 请求认证设置 */} + + setAuthConfigEnabled(e)} /> + - {/* 路由降级策略 */} + {/* 允许请求本路由的消费者名称列表 */} + {authConfig_enabled ? +
)} > - + { + consumerList.map((item) => { + return ( + + {item.name} + + ) + }) + } - - :null - } - {/* 请求认证设置 */} - - setAuthConfigEnabled(e)}/> - - - {/* 允许请求本路由的消费者名称列表 */} - {authConfig_enabled? -
- )} - > - - - - -
- :null} + +
+ : null} ); }); diff --git a/frontend/src/pages/ai/provider.tsx b/frontend/src/pages/ai/provider.tsx index 3737b5eb..3a5c0b47 100644 --- a/frontend/src/pages/ai/provider.tsx +++ b/frontend/src/pages/ai/provider.tsx @@ -105,7 +105,8 @@ const LlmProviderList: React.FC = () => { if (currentLlmProvider) { // version 进行创建或强制更新操作时需设置为 0。 - await updateLlmProvider({ version: 0, ...values } as LlmProvider); + const params: LlmProvider = { version: 0, ...values }; + await updateLlmProvider(params); } else { await addLlmProvider(values as LlmProvider); } @@ -113,7 +114,11 @@ const LlmProviderList: React.FC = () => { setOpenDrawer(false); formRef.current && formRef.current.reset(); refresh(); - } catch (errInfo) {} + } catch (errInfo) { + setOpenDrawer(false); + formRef.current && formRef.current.reset(); + refresh(); + } }; const handleDrawerCancel = () => { @@ -129,13 +134,20 @@ const LlmProviderList: React.FC = () => { const handleModalOk = async () => { setConfirmLoading(true); - await deleteLlmProvider(currentLlmProvider.name); + try { + await deleteLlmProvider(currentLlmProvider.name); + } catch (err) { + setConfirmLoading(false); + setOpenModal(false); + refresh(); + } setConfirmLoading(false); setOpenModal(false); refresh(); }; const handleModalCancel = () => { + setConfirmLoading(false); setOpenModal(false); setCurrentLlmProvider(null); }; diff --git a/frontend/src/pages/ai/route.tsx b/frontend/src/pages/ai/route.tsx index bbc26f03..86113370 100644 --- a/frontend/src/pages/ai/route.tsx +++ b/frontend/src/pages/ai/route.tsx @@ -125,11 +125,11 @@ const AiRouteList: React.FC = () => { const handleDrawerOK = async () => { try { const values = formRef.current ? await formRef.current.handleSubmit() : {}; - console.log('values', values) - if(!values) return false; + if (!values) return false; if (currentAiRoute) { - await updateAiRoute({ version: currentAiRoute.version, ...values } as AiRoute); + const params: AiRoute = { version: currentAiRoute.version, ...values }; + await updateAiRoute(params); } else { await addAiRoute(values as AiRoute); } @@ -138,7 +138,9 @@ const AiRouteList: React.FC = () => { formRef.current && formRef.current.reset(); refresh(); } catch (errInfo) { - console.log('Save failed: ', errInfo); + setOpenDrawer(false); + formRef.current && formRef.current.reset(); + refresh(); } }; @@ -162,6 +164,7 @@ const AiRouteList: React.FC = () => { }; const handleModalCancel = () => { + setConfirmLoading(false); setOpenModal(false); setCurrentAiRoute(null); }; @@ -206,6 +209,7 @@ const AiRouteList: React.FC = () => { title={t(currentAiRoute ? "aiRoute.edit" : "aiRoute.create")} placement="right" width={660} + destroyOnClose onClose={handleDrawerCancel} open={openDrawer} extra={ @@ -229,8 +233,8 @@ const AiRouteList: React.FC = () => { okText={t('misc.confirm')} >

- - 确定删除 {{ currentAiRouteName: (currentAiRoute && currentAiRoute.name) || '' }} 吗? + + 确定删除 {{ currentRouteName: (currentAiRoute && currentAiRoute.name) || '' }} 吗?

From 6aa7529a5c402f9d2bb100b07fa73a2999961d17 Mon Sep 17 00:00:00 2001 From: xpf <965732851@qq.com> Date: Tue, 12 Nov 2024 11:07:54 +0800 Subject: [PATCH 03/17] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0=E9=83=A8=E5=88=86?= =?UTF-8?q?=E5=9B=BD=E9=99=85=E5=8C=96,=E5=AE=8C=E5=96=84=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/locales/en-US/translation.json | 55 ++++++++++++++ frontend/src/locales/zh-CN/translation.json | 35 ++++++++- .../ai/components/RouteForm/Components.tsx | 12 ++- .../pages/ai/components/RouteForm/index.tsx | 74 ++++++++++--------- frontend/src/pages/ai/route.tsx | 36 +++++++-- 5 files changed, 163 insertions(+), 49 deletions(-) diff --git a/frontend/src/locales/en-US/translation.json b/frontend/src/locales/en-US/translation.json index 1072242a..c297f62c 100644 --- a/frontend/src/locales/en-US/translation.json +++ b/frontend/src/locales/en-US/translation.json @@ -101,6 +101,61 @@ "item4": "Input the HTTPS URL of Grafana is highly recommended to ensure the dashboard can be shown with in this page correctly." } }, + "llmProvider": { + "Columns": { + "Type": "Type", + "Name": "Name", + "Tokens": "authentication token", + "Action": "action" + }, + "ProviderTypes": { + "OpenAI": "OpenAI", + "qwen": "Tongyi Qianwen", + "Moonshot": "Dark Side of the Moon" + }, + "providerForm": { + "label": { + "Type": "Type", + "Name": "Name", + "Domain": "Domain name", + "modelPredicate": "Whether to enable model-based route matching rules", + "Extra": "When enabled, if the request to the target service fails, the gateway will instead request a downgraded service.", + "modelPredicatePrefix": "Match the model name prefix required for this route", + "aiName": "Target AI Service", + "aiNameExtra": "Create AI services", + "fallbackConfig": "Downgrade service", + "FallbackConfigExtra": "When enabled, if the request to the target service fails, the gateway will instead request a downgraded service.", + "fallbackConfigList": "Downgrade service list", + "RouteStrategy": "Routing downgrade strategy", + "routeStrategy1": "Randomly select a service provider in the list to downgrade, only downgrade once (default) ", + "routeStrategy2": "Attempt downgrades one by one in the order of the list of service providers until the request succeeds or all providers have tried", + "AuthConfig": "Whether to enable request authentication", + "AuthConfigExtra": "When enabled, only requests containing the specified consumer authentication information can request this route.", + "AuthConfigList": "List of consumer names allowed to request this route", + "Weight": "weight" + }, + "Placeholder": { + "Type": "Please select type", + "Name": "Please enter a name", + "Tokens": "Please enter authentication token", + "Domain": "Please enter domain name", + "aiName": "Please select and add target AI service", + "modelPredicatePrefix": "Please select the model name prefix required to match this route", + "fallbackConfigList": "Please select a list of downgrade services", + "RouteStrategy": "Please select a route degradation strategy", + "AuthConfigList": "Please select a list of consumer names that are allowed to request this route." + }, + "Rules": { + "modelPredicatePrefix": "Matches the model name prefix required by this route (excluding the ending '/') ", + "Name": "Supports uppercase and lowercase letters, numbers, and dashes, and starts with a letter or number and does not end with a dash (-). Length should not exceed 63 characters." + }, + "creatDomain": "Create a domain name" + }, + "Create": "Provide AI service providers", + "Edit": "Edit AI service provider", + "deleteConfirmation": "Are you sure you want to delete < 1 > {{currentLlmProviderName}} ?", + "DeleteRoute": "Are you sure you want to delete < 1 > {{currentRouteName}} ?" + }, "plugins": { "title": "Strategy Configuration", "subTitle": { diff --git a/frontend/src/locales/zh-CN/translation.json b/frontend/src/locales/zh-CN/translation.json index 1c459c6d..1648de0d 100644 --- a/frontend/src/locales/zh-CN/translation.json +++ b/frontend/src/locales/zh-CN/translation.json @@ -144,20 +144,47 @@ "providerForm": { "label": { "type": "类型", - "name": "名称" + "name": "名称", + "domain": "域名", + "modelPredicate": "是否启用基于模型的路由匹配规则", + "extra": "启用后,若请求目标服务失败,网关会改为请求降级服务。", + "modelPredicatePrefix": "匹配本路由所需的模型名称前缀", + "aiName": "目标AI服务", + "aiNameExtra": "创建AI服务", + "fallbackConfig": "降级服务", + "fallbackConfigExtra": "启用后,若请求目标服务失败,网关会改为请求降级服务。", + "fallbackConfigList": "降级服务列表", + "routeStrategy": "路由降级策略", + "routeStrategy1": "随机选择列表中的一个服务提供商进行降级,只降级一次(默认)", + "routeStrategy2": "按服务提供商列表顺序逐个尝试降级,直至请求成功或所有提供商均尝试过为止", + "authConfig": "是否启用请求认证", + "authConfigExtra": "启用后,只有包含指定消费者认证信息的请求可以请求本路由。", + "authConfigList": "允许请求本路由的消费者名称列表", + "weight": "权重" }, "placeholder": { "type": "请选择类型", "name": "请输入名称", - "tokens": "请输入认证令牌" + "tokens": "请输入认证令牌", + "domain": "请输入域名", + "aiName": "请选择并添加目标AI服务", + "modelPredicatePrefix": "请选择匹配本路由所需的模型名称前缀", + "fallbackConfigList": "请选择降级服务列表", + "routeStrategy": "请选择路由降级策略", + "authConfigList": "请选择允许请求本路由的消费者名称列表" }, "rules": { + "modelPredicatePrefix": "匹配本路由所需的模型名称前缀(不包含结尾的“/”)", "name": "支持大小写字母、数字和短划线,并以字母或数字开头,且不以短划线(-)结尾。长度不超过63个字符。" - } + }, + "creatDomain": "创建域名", + "howtouse": "使用方法", + "AirouterUse": "AI路由使用方法" }, "create": "提供AI服务提供者", "edit": "编辑AI服务提供者", - "deleteConfirmation": "确定删除 <1>{{currentLlmProviderName}} 吗?" + "deleteConfirmation": "确定删除 <1>{{currentLlmProviderName}} 吗?", + "deleteRoute": "确定删除 <1>{{currentRouteName}} 吗?" }, "plugins": { "title": "策略配置", diff --git a/frontend/src/pages/ai/components/RouteForm/Components.tsx b/frontend/src/pages/ai/components/RouteForm/Components.tsx index cd16ffca..52c790f9 100644 --- a/frontend/src/pages/ai/components/RouteForm/Components.tsx +++ b/frontend/src/pages/ai/components/RouteForm/Components.tsx @@ -3,6 +3,7 @@ import { Form, Button, Table, InputNumber } from 'antd'; import { RedoOutlined } from '@ant-design/icons'; import { history } from 'ice'; import { CustomComponentHandles } from '@/interfaces/service-source'; +import { useTranslation } from 'react-i18next'; // 刷新按钮 const RedoOutlinedBtn = (props) => { @@ -39,6 +40,7 @@ const HistoryButton = (props) => { }; const UpstreamsTable = React.forwardRef((props, ref) => { + const { t } = useTranslation(); const [dataSource, setDataSource] = useState([]); useImperativeHandle(ref, () => ({ @@ -53,12 +55,12 @@ const UpstreamsTable = React.forwardRef((props, ref) => const columns = [ { - title: '服务', + title: t('aiRoute.columns.upstreams'), // '服务', dataIndex: 'provider', key: 'provider', }, { - title: '权重', + title: t('llmProvider.providerForm.label.weight'), // '权重', dataIndex: 'weight', key: 'weight', render: (_, record) => ( @@ -73,11 +75,13 @@ const UpstreamsTable = React.forwardRef((props, ref) => ), }, { - title: '操作', + title: t('plugins.builtIns.headerControl.action'), // 操作 dataIndex: 'operation', key: 'operation', render: (_, record) => ( - + ), }, ]; diff --git a/frontend/src/pages/ai/components/RouteForm/index.tsx b/frontend/src/pages/ai/components/RouteForm/index.tsx index 9f784e86..a7808aa2 100644 --- a/frontend/src/pages/ai/components/RouteForm/index.tsx +++ b/frontend/src/pages/ai/components/RouteForm/index.tsx @@ -1,8 +1,8 @@ -import { ServiceSourceTypes, CustomComponentHandles } from '@/interfaces/service-source'; -import { Form, Input, Select, Tabs, Switch, Button } from 'antd'; +import { CustomComponentHandles } from '@/interfaces/service-source'; +import { Form, Input, Select, Switch, Button } from 'antd'; import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { RedoOutlined, PlusOutlined } from '@ant-design/icons'; +import { PlusOutlined } from '@ant-design/icons'; import { LlmProvider } from '@/interfaces/llm-provider'; import { getLlmProviders } from '@/services/llm-provider'; import { Domain } from '@/interfaces/domain'; @@ -12,7 +12,8 @@ import { useRequest } from 'ahooks'; import { getGatewayDomains } from '@/services'; import { RedoOutlinedBtn, HistoryButton, UpstreamsTable } from './Components'; -const ConsumerForm: React.FC = forwardRef((props, ref) => { + +const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { const { t } = useTranslation(); const { value } = props; const [form] = Form.useForm(); @@ -168,14 +169,15 @@ const ConsumerForm: React.FC = forwardRef((props, ref) => { rules={[ { required: true, - message: t('llmProvider.providerForm.placeholder.name'), + pattern: /^(?!-)[A-Za-z0-9-]{0,63}[A-Za-z0-9]$/, + message: t('serviceSource.serviceSourceForm.nameRequired'), }, ]} > @@ -185,19 +187,19 @@ const ConsumerForm: React.FC = forwardRef((props, ref) => {
)} + extra={()} > + { llmList.map((item) => { @@ -357,20 +359,20 @@ const ConsumerForm: React.FC = forwardRef((props, ref) => { {/* 路由降级策略 */} - + {t('llmProvider.providerForm.label.routeStrategy1')} + {t('llmProvider.providerForm.label.routeStrategy')} @@ -380,10 +382,10 @@ const ConsumerForm: React.FC = forwardRef((props, ref) => { {/* 请求认证设置 */} setAuthConfigEnabled(e)} /> @@ -393,19 +395,19 @@ const ConsumerForm: React.FC = forwardRef((props, ref) => {
)} + extra={()} > @@ -164,7 +164,7 @@ const ProviderForm: React.FC = forwardRef((props, ref) => { showCount allowClear maxLength={200} - disabled={value} + disabled={props.value} placeholder={t('llmProvider.providerForm.rules.name')} /> @@ -176,8 +176,8 @@ const ProviderForm: React.FC = forwardRef((props, ref) => { name="protocol" initialValue="openai/v1" > - {/* 删除按钮 */} - {fields.length > 1 ? - (
)} > - + + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }) => ( + + + + + + + + + + remove(name)} /> + + + ))} + + + + + )} + {/* 降级服务 */} @@ -413,7 +449,7 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { consumerList.map((item) => { return ( {item.name} diff --git a/frontend/src/pages/ai/config/index.tsx b/frontend/src/pages/ai/config/index.tsx new file mode 100644 index 00000000..8b179840 --- /dev/null +++ b/frontend/src/pages/ai/config/index.tsx @@ -0,0 +1,68 @@ +import { PageContainer } from '@ant-design/pro-layout'; +import { Button, Col, PageHeader, Row, Spin } from 'antd'; +import { RedoOutlined } from '@ant-design/icons'; +import { history, useSearchParams } from 'ice'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export default function RouterConfig() { + const { t } = useTranslation(); + const [searchParams] = useSearchParams(); + const name = searchParams.get('name'); + + const handleBack = () => { + history?.push('/ai/route'); + }; + + const pageHeader = useMemo(() => { + return { title: '策略配置', subTitle: `AI 路由名称 ${name}` }; + }, [name]); + + return ( +
+ {!!pageHeader.title && ( + + )} + + +
+ +
+ + + + - ), - }, - ]; - - const deleteData = (record) => { - const newList = dataSource.filter(i => i.provider !== record.provider); - setDataSource(newList); - }; - - const weightChange = (val, record) => { - const newList = dataSource.map(i => { - if (i.provider === record.provider) { - return { - ...i, - weight: val, - } - } - return i; - }); - - setDataSource(newList); - }; - - return ( - <> -
- - ); -}); - export { RedoOutlinedBtn, HistoryButton, - UpstreamsTable, }; diff --git a/frontend/src/pages/ai/components/RouteForm/index.tsx b/frontend/src/pages/ai/components/RouteForm/index.tsx index 26edb5f5..df20fbea 100644 --- a/frontend/src/pages/ai/components/RouteForm/index.tsx +++ b/frontend/src/pages/ai/components/RouteForm/index.tsx @@ -11,16 +11,15 @@ import { useRequest } from 'ahooks'; import { getGatewayDomains } from '@/services'; import { RedoOutlinedBtn, HistoryButton } from './Components'; - const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { const { t } = useTranslation(); const { value } = props; const [form] = Form.useForm(); - const [modelPredicate_enabled, setModelPredicateEnabled] = useState(false); const [fallbackConfig_enabled, setFallbackConfigEnabled] = useState(false); const [authConfig_enabled, setAuthConfigEnabled] = useState(false); // 目标AI服务错误提示 const [upstreamsError, setUpstreamsError] = useState(false); + const [modelService, setModelService] = useState('Proportion'); const [llmList, setLlmList] = useState([]); const llmResult = useRequest(getLlmProviders, { @@ -63,26 +62,23 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { return () => { setAuthConfigEnabled(false); setFallbackConfigEnabled(false); - setModelPredicateEnabled(false); } }, []); const initForm = () => { const { name = "", domains, upstreams = [] } = value; - const _modelPredicate_enabled = value?.modelPredicate?.enabled || false; const _authConfig_enabled = value?.authConfig?.enabled || false; const _fallbackConfig_enabled = value?.fallbackConfig?.enabled || false; setAuthConfigEnabled(_authConfig_enabled); setFallbackConfigEnabled(_fallbackConfig_enabled); - setModelPredicateEnabled(_modelPredicate_enabled); + setModelService(value?.modelPredicate?.enabled ? "Proportion" : "ModelName") form.setFieldsValue({ name, domains: domains?.length ? domains[0] : undefined, - upstreams, - modelPredicate_enabled: _modelPredicate_enabled, + upstreams: value?.modelPredicate?.enabled ? upstreams[0].provider : upstreams, modelPredicate_prefix: value?.modelPredicate?.prefix || "", authConfig_enabled: _authConfig_enabled, authConfig_allowedConsumers: value?.authConfig?.allowedConsumers || "", @@ -100,19 +96,21 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { const values = await form.validateFields(); const { upstreams = [] } = values; - if (!upstreams?.length) { - setUpstreamsError("aiName"); - return false; - } - - // 判断 AI 服务权重相加是否等于 100 - const sumWeights: any = upstreams.reduce((accumulator, currentObject) => { - return parseInt(accumulator) + parseInt(currentObject.weight); - }, 0) - - if (sumWeights !== 100) { - setUpstreamsError('weight'); - return false; + if (modelService === "ModelName") { + if (!upstreams?.length) { + setUpstreamsError("aiName"); + return false; + } + + // 判断 AI 服务权重相加是否等于 100 + const sumWeights: any = upstreams.reduce((accumulator, currentObject) => { + return parseInt(accumulator, 10) + parseInt(currentObject.weight, 10); + }, 0) + + if (sumWeights !== 100) { + setUpstreamsError('weight'); + return false; + } } setUpstreamsError(false); @@ -126,16 +124,17 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { authConfig_allowedConsumers = '', } = values; + const isProportion = modelService === "Proportion"; const payload = { name, domains: domains ? [domains] : [], modelPredicate: { - enabled: values?.modelPredicate_enabled, + enabled: isProportion, }, - upstreams, + upstreams: isProportion ? [{ provider: upstreams, weight: 100 }] : upstreams, fallbackConfig: { enabled: fallbackConfig_enabled, @@ -146,7 +145,7 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { }, }; - if (values?.modelPredicate_enabled) { + if (isProportion) { payload['modelPredicate']['prefix'] = modelPredicate_prefix; } @@ -199,6 +198,7 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { extra={()} > { + await form.resetFields(["upstreams", "modelPredicate_prefix"]); + setModelService(e); + }} + value={modelService} + allowClear + style={{ width: "100%", marginBottom: 24 }} > - setModelPredicateEnabled(e)} /> - + 按比例 + 按请求 Body 中的模型名称 + - {/* 匹配本路由所需的模型名称前缀(不包含结尾的“/”) */} { - modelPredicate_enabled ? - - - : null - } + modelService === "Proportion" ? + <> + + )} + rules={[ + { + required: true, + message: t('llmProvider.providerForm.placeholder.aiName'), + }, + ]} + > + + - {/* 目标AI服务 upstreams */} - )} - > - - {(fields, { add, remove }) => ( - <> - {fields.map(({ key, name, ...restField }) => ( - - - - - - - - - - remove(name)} /> - - - ))} - - + + - - )} - - + + : + <> + {/* 目标AI服务 upstreams */} + )} + > + + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }) => ( + + + + + + + + + + remove(name)} /> + + + ))} + + + + + )} + + + + } {/* 降级服务 */} { initialValue={false} extra={t('llmProvider.providerForm.label.fallbackConfigExtra')} > - setFallbackConfigEnabled(e)} /> + { + setFallbackConfigEnabled(e) + form.resetFields(["fallbackConfig_upstreams", "fallbackConfig_strategy"]) + }} + /> { @@ -371,20 +401,13 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { }, ]} > - { - llmList.map((item) => { - return ( - - {item.name} - - ) - }) + llmList.map((item) => ( + + {item.name} + + )) } @@ -406,7 +429,7 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { }, ]} > - {t('llmProvider.providerForm.label.routeStrategy1')} {t('llmProvider.providerForm.label.routeStrategy')} @@ -423,7 +446,11 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { initialValue={false} extra={t('llmProvider.providerForm.label.authConfigExtra')} > - setAuthConfigEnabled(e)} /> + { + setAuthConfigEnabled(e) + form.resetFields(["authConfig_allowedConsumers"]) + }} + /> {/* 允许请求本路由的消费者名称列表 */} @@ -442,16 +469,11 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { ]} extra={()} > - { consumerList.map((item) => { return ( - + {item.name} ) From 96d36265b3b5015bf90a2feb1be05868a53476c6 Mon Sep 17 00:00:00 2001 From: xpf <965732851@qq.com> Date: Wed, 13 Nov 2024 14:38:54 +0800 Subject: [PATCH 08/17] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E5=9B=BD?= =?UTF-8?q?=E9=99=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/locales/en-US/translation.json | 67 ++++++++++--------- frontend/src/locales/zh-CN/translation.json | 6 +- .../pages/ai/components/RouteForm/index.tsx | 8 +-- 3 files changed, 46 insertions(+), 35 deletions(-) diff --git a/frontend/src/locales/en-US/translation.json b/frontend/src/locales/en-US/translation.json index c297f62c..f43f8c1a 100644 --- a/frontend/src/locales/en-US/translation.json +++ b/frontend/src/locales/en-US/translation.json @@ -102,59 +102,66 @@ } }, "llmProvider": { - "Columns": { - "Type": "Type", - "Name": "Name", - "Tokens": "authentication token", - "Action": "action" + "columns": { + "type": "Type", + "name": "Name", + "tokens": "authentication token", + "action": "action" }, - "ProviderTypes": { - "OpenAI": "OpenAI", + "providerTypes": { + "openai": "OpenAI", "qwen": "Tongyi Qianwen", - "Moonshot": "Dark Side of the Moon" + "moonshot": "Dark Side of the Moon" }, "providerForm": { "label": { - "Type": "Type", - "Name": "Name", - "Domain": "Domain name", + "type": "Type", + "name": "Name", + "domain": "Domain name", "modelPredicate": "Whether to enable model-based route matching rules", - "Extra": "When enabled, if the request to the target service fails, the gateway will instead request a downgraded service.", + "extra": "When enabled, if the request to the target service fails, the gateway will instead request a downgraded service.", "modelPredicatePrefix": "Match the model name prefix required for this route", "aiName": "Target AI Service", "aiNameExtra": "Create AI services", "fallbackConfig": "Downgrade service", - "FallbackConfigExtra": "When enabled, if the request to the target service fails, the gateway will instead request a downgraded service.", + "fallbackConfigExtra": "When enabled, if the request to the target service fails, the gateway will instead request a downgraded service.", "fallbackConfigList": "Downgrade service list", - "RouteStrategy": "Routing downgrade strategy", + "routeStrategy": "Routing downgrade strategy", "routeStrategy1": "Randomly select a service provider in the list to downgrade, only downgrade once (default) ", "routeStrategy2": "Attempt downgrades one by one in the order of the list of service providers until the request succeeds or all providers have tried", - "AuthConfig": "Whether to enable request authentication", - "AuthConfigExtra": "When enabled, only requests containing the specified consumer authentication information can request this route.", - "AuthConfigList": "List of consumer names allowed to request this route", - "Weight": "weight" + "authConfig": "Whether to enable request authentication", + "authConfigExtra": "When enabled, only requests containing the specified consumer authentication information can request this route.", + "authConfigList": "List of consumer names allowed to request this route", + "weight": "weight" }, - "Placeholder": { - "Type": "Please select type", - "Name": "Please enter a name", - "Tokens": "Please enter authentication token", - "Domain": "Please enter domain name", + "placeholder": { + "type": "Please select type", + "name": "Please enter a name", + "tokens": "Please enter authentication token", + "domain": "Please enter domain name", "aiName": "Please select and add target AI service", + "weight": "The sum of the target AI service weights should equal 100.", "modelPredicatePrefix": "Please select the model name prefix required to match this route", "fallbackConfigList": "Please select a list of downgrade services", - "RouteStrategy": "Please select a route degradation strategy", - "AuthConfigList": "Please select a list of consumer names that are allowed to request this route." + "routeStrategy": "Please select a route degradation strategy", + "authConfigList": "Please select a list of consumer names that are allowed to request this route." }, - "Rules": { + "rules": { "modelPredicatePrefix": "Matches the model name prefix required by this route (excluding the ending '/') ", "Name": "Supports uppercase and lowercase letters, numbers, and dashes, and starts with a letter or number and does not end with a dash (-). Length should not exceed 63 characters." }, - "creatDomain": "Create a domain name" + "creatDomain": "Create a domain name", + "howtouse": "How to use", + "AirouterUse": "How to use AI routing" }, - "Create": "Provide AI service providers", - "Edit": "Edit AI service provider", + "create": "Provide AI service providers", + "edit": "Edit AI service provider", "deleteConfirmation": "Are you sure you want to delete < 1 > {{currentLlmProviderName}} ?", - "DeleteRoute": "Are you sure you want to delete < 1 > {{currentRouteName}} ?" + "deleteRoute": "Are you sure you want to delete < 1 > {{currentRouteName}} ?", + "selectModelName": "Select Model Service", + "modelProportion": "proportionally", + "modelName": "By Model Name in Request Body", + "addTargetAIservice": "Add target AI service" }, "plugins": { "title": "Strategy Configuration", diff --git a/frontend/src/locales/zh-CN/translation.json b/frontend/src/locales/zh-CN/translation.json index b370c61b..d00a4c12 100644 --- a/frontend/src/locales/zh-CN/translation.json +++ b/frontend/src/locales/zh-CN/translation.json @@ -185,7 +185,11 @@ "create": "提供AI服务提供者", "edit": "编辑AI服务提供者", "deleteConfirmation": "确定删除 <1>{{currentLlmProviderName}} 吗?", - "deleteRoute": "确定删除 <1>{{currentRouteName}} 吗?" + "deleteRoute": "确定删除 <1>{{currentRouteName}} 吗?", + "selectModelName": "选择模型服务", + "modelProportion": "按比例", + "modelName": "按请求 Body 中的模型名称", + "addTargetAIservice": "添加目标AI服务" }, "plugins": { "title": "策略配置", diff --git a/frontend/src/pages/ai/components/RouteForm/index.tsx b/frontend/src/pages/ai/components/RouteForm/index.tsx index df20fbea..372874d0 100644 --- a/frontend/src/pages/ai/components/RouteForm/index.tsx +++ b/frontend/src/pages/ai/components/RouteForm/index.tsx @@ -219,7 +219,7 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { -
选择模型服务
+
{t("llmProvider.selectModelName")}
{ @@ -359,7 +359,7 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { add() }} > - 添加目标AI服务 + {/* 添加目标AI服务 */}{t("llmProvider.addTargetAIservice")} From 3b0ffcc09d52e3df29f9308e512e841e465eff7b Mon Sep 17 00:00:00 2001 From: xpf <965732851@qq.com> Date: Wed, 11 Dec 2024 17:36:56 +0800 Subject: [PATCH 09/17] =?UTF-8?q?feat:=20Ai=E8=B7=AF=E7=94=B1=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/locales/en-US/translation.json | 21 +- frontend/src/locales/zh-CN/translation.json | 26 +- .../ai/components/ProviderForm/index.tsx | 32 +- .../pages/ai/components/RouteForm/index.tsx | 409 ++++++++++-------- frontend/src/pages/ai/route.tsx | 4 + 5 files changed, 288 insertions(+), 204 deletions(-) diff --git a/frontend/src/locales/en-US/translation.json b/frontend/src/locales/en-US/translation.json index f43f8c1a..ea431f7d 100644 --- a/frontend/src/locales/en-US/translation.json +++ b/frontend/src/locales/en-US/translation.json @@ -115,12 +115,14 @@ }, "providerForm": { "label": { - "type": "Type", + "type": "LLM vendor", "name": "Name", + "serviceName": "Service Name", "domain": "Domain name", + "agreement": "Agreement", "modelPredicate": "Whether to enable model-based route matching rules", "extra": "When enabled, if the request to the target service fails, the gateway will instead request a downgraded service.", - "modelPredicatePrefix": "Match the model name prefix required for this route", + "modelPredicatePrefix": "model name matching rule", "aiName": "Target AI Service", "aiNameExtra": "Create AI services", "fallbackConfig": "Downgrade service", @@ -135,19 +137,20 @@ "weight": "weight" }, "placeholder": { - "type": "Please select type", + "type": "Please select LLM vendor", "name": "Please enter a name", + "serviceName": "Please enter a service name", "tokens": "Please enter authentication token", "domain": "Please enter domain name", "aiName": "Please select and add target AI service", - "weight": "The sum of the target AI service weights should equal 100.", - "modelPredicatePrefix": "Please select the model name prefix required to match this route", + "weight": "The target AI service request ratio should add up to 100.", + "modelPredicatePrefix": "Use Glob syntax to match models, for example: model- *", "fallbackConfigList": "Please select a list of downgrade services", "routeStrategy": "Please select a route degradation strategy", "authConfigList": "Please select a list of consumer names that are allowed to request this route." }, "rules": { - "modelPredicatePrefix": "Matches the model name prefix required by this route (excluding the ending '/') ", + "modelPredicatePrefix": "Please enter the model name prefix required to match this route", "Name": "Supports uppercase and lowercase letters, numbers, and dashes, and starts with a letter or number and does not end with a dash (-). Length should not exceed 63 characters." }, "creatDomain": "Create a domain name", @@ -159,8 +162,8 @@ "deleteConfirmation": "Are you sure you want to delete < 1 > {{currentLlmProviderName}} ?", "deleteRoute": "Are you sure you want to delete < 1 > {{currentRouteName}} ?", "selectModelName": "Select Model Service", - "modelProportion": "proportionally", - "modelName": "By Model Name in Request Body", + "modelProportion": "Proportionally", + "modelName": "By model name", "addTargetAIservice": "Add target AI service" }, "plugins": { @@ -304,7 +307,7 @@ "namePlaceholder": "Please input a valid name.", "domain": "Registry Domain", "domainTooltip": "It can be either an IP address or a domain name.", - "domainRequired": "Please input the registry address.", + "domainRequired": "Please select a domain name", "domainPlaceholder": "Only following inputs are allowed: upper-and lower-case letters, numbers, dash (-) and asterisk (*). No longer than 256 characters.", "port": "Registry Port", "portRequired": "Please input the port.", diff --git a/frontend/src/locales/zh-CN/translation.json b/frontend/src/locales/zh-CN/translation.json index d00a4c12..5774b581 100644 --- a/frontend/src/locales/zh-CN/translation.json +++ b/frontend/src/locales/zh-CN/translation.json @@ -133,7 +133,7 @@ "columns": { "type": "类型", "name": "名称", - "tokens": "认证令牌", + "tokens": "凭证", "action": "操作" }, "providerTypes": { @@ -143,12 +143,14 @@ }, "providerForm": { "label": { - "type": "类型", + "type": "大模型供应商", "name": "名称", + "serviceName": "服务名称", + "agreement": "协议", "domain": "域名", "modelPredicate": "是否启用基于模型的路由匹配规则", "extra": "启用后,若请求目标服务失败,网关会改为请求降级服务。", - "modelPredicatePrefix": "匹配本路由所需的模型名称前缀", + "modelPredicatePrefix": "模型名称匹配规则", "aiName": "目标AI服务", "aiNameExtra": "创建AI服务", "fallbackConfig": "降级服务", @@ -160,22 +162,24 @@ "authConfig": "是否启用请求认证", "authConfigExtra": "启用后,只有包含指定消费者认证信息的请求可以请求本路由。", "authConfigList": "允许请求本路由的消费者名称列表", - "weight": "权重" + "weight": "请求比例" }, "placeholder": { - "type": "请选择类型", + "type": "请选择大模型供应商", "name": "请输入名称", - "tokens": "请输入认证令牌", + "agreement": "请选择协议", + "serviceName": "请输入服务名称", + "tokens": "请输入凭证", "domain": "请输入域名", "aiName": "请选择并添加目标AI服务", - "weight": "目标AI服务权重相加应等于 100", - "modelPredicatePrefix": "匹配本路由所需的模型名称前缀(不包含结尾的“/”)", + "weight": "目标AI服务请求比例相加应等于 100", + "modelPredicatePrefix": "使用 Glob 语法匹配模型,例如:model-*", "fallbackConfigList": "请选择降级服务列表", "routeStrategy": "请选择路由降级策略", "authConfigList": "请选择允许请求本路由的消费者名称列表" }, "rules": { - "modelPredicatePrefix": "匹配本路由所需的模型名称前缀(不包含结尾的“/”)", + "modelPredicatePrefix": "请输入匹配本路由所需的模型名称前缀", "name": "支持大小写字母、数字和短划线,并以字母或数字开头,且不以短划线(-)结尾。长度不超过63个字符。" }, "creatDomain": "创建域名", @@ -188,7 +192,7 @@ "deleteRoute": "确定删除 <1>{{currentRouteName}} 吗?", "selectModelName": "选择模型服务", "modelProportion": "按比例", - "modelName": "按请求 Body 中的模型名称", + "modelName": "按模型名称", "addTargetAIservice": "添加目标AI服务" }, "plugins": { @@ -332,7 +336,7 @@ "namePlaceholder": "支持大小写字母、数字和短划线,并以字母或数字开头,且不以短划线结尾。长度不超过63个字符。", "domain": "注册中心地址", "domainTooltip": "注册中心地址,可以是IP或域名", - "domainRequired": "请输入注册中心地址", + "domainRequired": "请选择域名", "domainPlaceholder": "支持大小写字母、数字、短划线(-)和星号(*),不超过256个字符。", "port": "注册中心访问端口", "portRequired": "请输入访问端口", diff --git a/frontend/src/pages/ai/components/ProviderForm/index.tsx b/frontend/src/pages/ai/components/ProviderForm/index.tsx index e064c126..86ae994d 100644 --- a/frontend/src/pages/ai/components/ProviderForm/index.tsx +++ b/frontend/src/pages/ai/components/ProviderForm/index.tsx @@ -11,6 +11,10 @@ const providerTypeDisplayName = [ { key: 'moonshot', label: 'llmProvider.providerTypes.moonshot' }, ]; +const agreementList = [ + { label: "Openai/v1", value: "openai/v1" }, +]; + const ProviderForm: React.FC = forwardRef((props: { value: any }, ref) => { const { t } = useTranslation(); const [form] = Form.useForm(); @@ -117,7 +121,7 @@ const ProviderForm: React.FC = forwardRef((props: { value: any }, ref) => { form={form} layout="vertical" > - {/* 类型 */} + {/* 大模型供应商 */} { - {/* 名称 */} + {/* 服务名称 */} @@ -168,25 +172,27 @@ const ProviderForm: React.FC = forwardRef((props: { value: any }, ref) => { /> - {/* 类型 */} + {/* 协议 */} - {/* 认证令牌 */} - + {/* 凭证 */} + {(fields, { add, remove }, { errors }) => ( <> {!fields.length ? diff --git a/frontend/src/pages/ai/components/RouteForm/index.tsx b/frontend/src/pages/ai/components/RouteForm/index.tsx index 372874d0..2cff0a61 100644 --- a/frontend/src/pages/ai/components/RouteForm/index.tsx +++ b/frontend/src/pages/ai/components/RouteForm/index.tsx @@ -1,4 +1,4 @@ -import { Form, Input, Select, Switch, Button, Space, InputNumber } from 'antd'; +import { Form, Input, Select, Switch, Button, Space, InputNumber, Table } from 'antd'; import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { PlusOutlined, MinusCircleOutlined } from '@ant-design/icons'; @@ -17,8 +17,7 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { const [form] = Form.useForm(); const [fallbackConfig_enabled, setFallbackConfigEnabled] = useState(false); const [authConfig_enabled, setAuthConfigEnabled] = useState(false); - // 目标AI服务错误提示 - const [upstreamsError, setUpstreamsError] = useState(false); + const [upstreamsError, setUpstreamsError] = useState(false); // 目标AI服务错误提示 const [modelService, setModelService] = useState('Proportion'); const [llmList, setLlmList] = useState([]); @@ -66,26 +65,46 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { }, []); const initForm = () => { - const { name = "", domains, upstreams = [] } = value; + const { name = "", domains, upstreams = [], modelPredicates } = value; const _authConfig_enabled = value?.authConfig?.enabled || false; const _fallbackConfig_enabled = value?.fallbackConfig?.enabled || false; setAuthConfigEnabled(_authConfig_enabled); setFallbackConfigEnabled(_fallbackConfig_enabled); - setModelService(value?.modelPredicate?.enabled ? "Proportion" : "ModelName") - - form.setFieldsValue({ + const initValues = { name, - domains: domains?.length ? domains[0] : undefined, - upstreams: value?.modelPredicate?.enabled ? upstreams[0].provider : upstreams, - modelPredicate_prefix: value?.modelPredicate?.prefix || "", + domains: domains?.length ? domains[0] : [], + upstreams, authConfig_enabled: _authConfig_enabled, authConfig_allowedConsumers: value?.authConfig?.allowedConsumers || "", fallbackConfig_enabled: _fallbackConfig_enabled, fallbackConfig_upstreams: value?.fallbackConfig?.upstreams ? value?.fallbackConfig?.upstreams[0].provider : undefined, fallbackConfig_strategy: value?.fallbackConfig?.strategy, - }) + }; + + if (modelPredicates) { + setModelService("ModelName"); + initValues["modelPredicates"] = modelPredicates.map(item => ({ + ...item, + provider: upstreams[0].provider, + })); + } else { + setModelService("Proportion"); + initValues["upstreams"] = upstreams.map((item) => { + let obj = { + provider: item.provider, + weight: item.weight, + }; + if (item.modelMapping) { + const _modelMapping = Object.keys(item.modelMapping); + obj["modelMapping"] = _modelMapping + } + return obj; + }); + } + + form.setFieldsValue(initValues); }; useImperativeHandle(ref, () => ({ @@ -96,7 +115,7 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { const values = await form.validateFields(); const { upstreams = [] } = values; - if (modelService === "ModelName") { + if (modelService === "Proportion") { if (!upstreams?.length) { setUpstreamsError("aiName"); return false; @@ -118,35 +137,35 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { const { name, domains, - modelPredicate_prefix = '', fallbackConfig_upstreams = '', fallbackConfig_strategy = '', authConfig_allowedConsumers = '', + modelPredicates = [], } = values; const isProportion = modelService === "Proportion"; const payload = { name, domains: domains ? [domains] : [], - - - modelPredicate: { - enabled: isProportion, - }, - - upstreams: isProportion ? [{ provider: upstreams, weight: 100 }] : upstreams, - fallbackConfig: { enabled: fallbackConfig_enabled, }, - authConfig: { enabled: authConfig_enabled, }, - }; + } if (isProportion) { - payload['modelPredicate']['prefix'] = modelPredicate_prefix; + payload["upstreams"] = upstreams.map(({ provider, weight, modelMapping }) => { + const obj = { provider, weight, modelMapping: {} }; + if (modelMapping) { + obj["modelMapping"][modelMapping] = provider; + } + return obj; + }); + } else { + payload["upstreams"] = modelPredicates.map(({ provider }) => ({ provider, weight: 100 })); + payload["modelPredicates"] = modelPredicates.map(({ matchType, matchValue }) => ({ matchType, matchValue })); } if (fallbackConfig_enabled) { @@ -201,89 +220,127 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { allowClear placeholder={t("serviceSource.serviceSourceForm.domainRequired")} > - { - domainsList.map((item) => { - return ( - - {item.name} - - ) - }) + { domainsList.map((item) => { + return ( + + {item.name} + + ) + }) } - -
{t("llmProvider.selectModelName")}
- + + { - modelService === "Proportion" ? + modelService === "ModelName" ? <> - - )} - rules={[ - { - required: true, - message: t('llmProvider.providerForm.placeholder.aiName'), - }, - ]} - > - - - - - - - + {/* 基于模型的路由匹配规则 */} + + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }) => { + return ( + <> +
(), + }, + { + title: "模型匹配方式", + dataIndex: 'matchValue', + render: () => ( + + + ), + }, + { + title: "模型名称", + dataIndex: 'matchType', + render: () => ( + + + ), + }, + ]} + bordered={false} + pagination={false} + /> + {/* 目标AI服务 */} + + + + + ) + })} + ) + } + : <> {/* 目标AI服务 upstreams */} { help={upstreamsError ? t(`llmProvider.providerForm.placeholder.${upstreamsError}`) : null} extra={()} > - - {(fields, { add, remove }) => ( - <> - {fields.map(({ key, name, ...restField }) => ( - - - + { llmList.map((item) => { const selectArr = form.getFieldValue('upstreams').map(i => i && i.provider) || []; return ( { ) }) - } - - - + + + {/* 请求比例 */} + + + + + {/* 模型名称 */} + + + + + + {index ? remove(name)} /> : null} + + + ))} + + - - - )} + {/* 添加目标AI服务 */}{t("llmProvider.addTargetAIservice")} + + + + ) + }} } - - {/* 降级服务 */} + >{/* 降级服务 */} { setFallbackConfigEnabled(e) form.resetFields(["fallbackConfig_upstreams", "fallbackConfig_strategy"]) @@ -387,27 +460,22 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { { fallbackConfig_enabled ? <> - {/* 降级服务列表 */} -
+
{/* 降级服务列表 */} @@ -453,8 +521,7 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { /> - {/* 允许请求本路由的消费者名称列表 */} - {authConfig_enabled ? + {authConfig_enabled ? // 允许请求本路由的消费者名称列表
{ setOpenModal(false); refresh(); } + handleModalCancel(); + setConfirmLoading(false); + setOpenModal(false); + refresh(); }; const handleModalCancel = () => { From 97045c26fa218829bb9de47623853ca5a04a60a2 Mon Sep 17 00:00:00 2001 From: xpf <965732851@qq.com> Date: Wed, 11 Dec 2024 18:20:51 +0800 Subject: [PATCH 10/17] feat: i18n --- frontend/src/locales/en-US/translation.json | 12 +- frontend/src/locales/zh-CN/translation.json | 12 +- .../pages/ai/components/RouteForm/index.tsx | 194 ++++++------------ 3 files changed, 85 insertions(+), 133 deletions(-) diff --git a/frontend/src/locales/en-US/translation.json b/frontend/src/locales/en-US/translation.json index ea431f7d..2e28b66d 100644 --- a/frontend/src/locales/en-US/translation.json +++ b/frontend/src/locales/en-US/translation.json @@ -164,7 +164,17 @@ "selectModelName": "Select Model Service", "modelProportion": "Proportionally", "modelName": "By model name", - "addTargetAIservice": "Add target AI service" + "addTargetAIservice": "Add target AI service", + "modelMatchingType": "Model matching type", + "pleaseSelect": "Please select", + "pleaseEnter": "Please enter", + "exactMatch": "exact match", + "prefixMatch": "Prefix Match", + "modelNames": "Model names", + "serviceName": "service name", + "requestPercentage": "Request Percentage", + "targetModel": "TargetModel", + "modelNameTips": "Model parameters in the request body" }, "plugins": { "title": "Strategy Configuration", diff --git a/frontend/src/locales/zh-CN/translation.json b/frontend/src/locales/zh-CN/translation.json index 5774b581..519ab87d 100644 --- a/frontend/src/locales/zh-CN/translation.json +++ b/frontend/src/locales/zh-CN/translation.json @@ -193,7 +193,17 @@ "selectModelName": "选择模型服务", "modelProportion": "按比例", "modelName": "按模型名称", - "addTargetAIservice": "添加目标AI服务" + "addTargetAIservice": "添加目标AI服务", + "modelMatchingType":"模型匹配方式", + "pleaseSelect":"请选择", + "pleaseEnter":"请输入", + "exactMatch":"精确匹配", + "prefixMatch":"前缀匹配", + "modelNames":"模型名称", + "serviceName":"服务名称", + "requestPercentage":"请求比例", + "targetModel":"目标模型", + "modelNameTips":"请求 body 中的 model 参数" }, "plugins": { "title": "策略配置", diff --git a/frontend/src/pages/ai/components/RouteForm/index.tsx b/frontend/src/pages/ai/components/RouteForm/index.tsx index 2cff0a61..a02e95fc 100644 --- a/frontend/src/pages/ai/components/RouteForm/index.tsx +++ b/frontend/src/pages/ai/components/RouteForm/index.tsx @@ -182,10 +182,7 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { })); return ( -
+ {/* 名称 */} { name="domains" extra={()} > - + { domainsList.map((item) => ({item.name}))} @@ -240,7 +224,7 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { ), }, - { - title: "模型匹配方式", + { // "模型匹配方式", + title: t("llmProvider.modelMatchingType"), dataIndex: 'matchValue', render: () => ( - - { [ - { name: "EQUAL", label: "精确匹配" }, - { name: "PRE", label: "前缀匹配" }, - ].map((item) => { - return ( - - {item.label} - - ) - }) + { name: "EQUAL", label: t("llmProvider.exactMatch") }, // 精确匹配 + { name: "PRE", label: t("llmProvider.prefixMatch") }, // 前缀匹配 + ].map((item) => ({item.label} )) } ), }, - { - title: "模型名称", + { // "模型名称", + title: t("llmProvider.modelNames"), dataIndex: 'matchType', render: () => ( - + ), }, ]} bordered={false} pagination={false} /> - {/* 目标AI服务 */} + rules={[{ required: true, message: t('llmProvider.providerForm.placeholder.aiName') }]} + >{/* 目标AI服务 */} @@ -351,43 +321,27 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { {(fields, { add, remove }) => { const baseStyle = { width: 190 }; - const requiredStyle = { - display: "inline-block", - marginRight: "4px", - color: "#ff4d4f", - fontFamily: "SimSun, sans-serif", - }; - + const requiredStyle = { display: "inline-block", marginRight: "4px", color: "#ff4d4f", fontFamily: "SimSun, sans-serif" }; return ( <> -
*服务名称
-
*请求比例
-
目标模型
+
*{t("llmProvider.serviceName")}
{/* 服务名称 */} +
*{t("llmProvider.requestPercentage")}
{/* 请求比例 */} +
{t("llmProvider.targetModel")}
{/* 目标模型 */}
{fields.map(({ key, name, ...restField }, index) => ( - {/* 服务名称 */} + rules={[{ required: true, message: t('llmProvider.providerForm.placeholder.aiName') }]} + >{/* 服务名称 */} - { llmList.map((item) => ( - - {item.name} - - )) - } - - - - -
- - {/* 路由降级策略 */} + {fallbackConfig_enabled ? + <> +
+ {/* 降级服务列表 */} - + { llmList.map((item) => ( {item.name} ))} - - : null + +
+ + {/* 路由降级策略 */} + + + + + : null } {/* 请求认证设置 */} @@ -525,30 +471,16 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => {
)} > -
: null} From 1bcbea686f51e5c89a18c7d9067fa38bc8111a08 Mon Sep 17 00:00:00 2001 From: xpf <965732851@qq.com> Date: Thu, 12 Dec 2024 17:01:49 +0800 Subject: [PATCH 11/17] =?UTF-8?q?feat:=E6=B6=88=E8=B4=B9=E8=80=85=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/locales/en-US/translation.json | 40 ++++++ frontend/src/locales/zh-CN/translation.json | 18 ++- .../pages/ai/components/RouteForm/index.tsx | 4 +- .../components/ConsumerForm/index.tsx | 129 ++++++++++++++++-- frontend/src/pages/consumer/index.tsx | 9 +- 5 files changed, 184 insertions(+), 16 deletions(-) diff --git a/frontend/src/locales/en-US/translation.json b/frontend/src/locales/en-US/translation.json index 2e28b66d..156e4e26 100644 --- a/frontend/src/locales/en-US/translation.json +++ b/frontend/src/locales/en-US/translation.json @@ -46,6 +46,46 @@ "passwordPlaceholder": "Password", "passwordRequired": "Please input password" }, + "aiRoute": { + "columns": { + "name": "name", + "domains": "domains", + "upstreams": "upstreams", + "auth": "request auth", + "action": "action" + }, + "create": "Create an AI route", + "edit": "Edit AI route", + "deleteConfirmation": "Are you sure you want to delete < 1 > {{currentConsumerName}} ?", + "authNotEnabled": "Authentication not enabled", + "authEnabledWithoutConsumer": "No one is authorized to access" + }, + "consumer": { + "columns": { + "name": "consumer name", + "credentialTypes": "authentication method", + "action": "action" + }, + "create": "create", + "edit": "edit", + "deleteConfirmation": "Are you sure you want to delete < 1 > {{currentConsumerName}} ?", + "consumerForm": { + "name": "name", + "namePlaceholder": "Please enter a consumer name", + "nameRequired": "Please enter a consumer name", + "pleaseEnter":"please enter", + "pleaseSelect":"please select" + }, + "selectBEARER": "Authorization: Bearer ${value}", + "selectHEADER": "Custom HTTP Header", + "selectQUERY": "query parameters", + "tokenSource":"Token source", + "authenticationToken":"Auth token", + "underDevelopment":"Under development, stay tuned.", + "randomGeneration":"random generation", + "headerName":"Header name", + "paramsName":"parameter name" + }, "domain": { "columns": { "name": "Domain", diff --git a/frontend/src/locales/zh-CN/translation.json b/frontend/src/locales/zh-CN/translation.json index 519ab87d..3afb1924 100644 --- a/frontend/src/locales/zh-CN/translation.json +++ b/frontend/src/locales/zh-CN/translation.json @@ -72,7 +72,23 @@ }, "create": "创建消费者", "edit": "编辑消费者", - "deleteConfirmation": "确定删除 <1>{{currentConsumerName}} 吗?" + "deleteConfirmation": "确定删除 <1>{{currentConsumerName}} 吗?", + "consumerForm": { + "name": "消费者名称", + "namePlaceholder": "请输入消费者名称", + "nameRequired": "请输入消费者名称", + "pleaseEnter":"请输入", + "pleaseSelect":"请输入" + }, + "selectBEARER": "Authorization: Bearer ${value}", + "selectHEADER": "自定义 HTTP Header", + "selectQUERY": "查询参数", + "tokenSource":"令牌来源", + "authenticationToken":"认证令牌", + "underDevelopment":"开发中,敬请期待", + "randomGeneration":"随机生成", + "headerName":"Header 名称", + "paramsName":"参数名称" }, "domain": { "columns": { diff --git a/frontend/src/pages/ai/components/RouteForm/index.tsx b/frontend/src/pages/ai/components/RouteForm/index.tsx index a02e95fc..b4de80b7 100644 --- a/frontend/src/pages/ai/components/RouteForm/index.tsx +++ b/frontend/src/pages/ai/components/RouteForm/index.tsx @@ -174,9 +174,9 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { } if (authConfig_enabled) { - payload['authConfig']['consuallowedConsumersmer'] = authConfig_allowedConsumers ? [authConfig_allowedConsumers] : []; + payload['authConfig']['allowedConsumers'] = authConfig_allowedConsumers ? [authConfig_allowedConsumers] : []; } - + console.log('payload', payload) return payload; }, })); diff --git a/frontend/src/pages/consumer/components/ConsumerForm/index.tsx b/frontend/src/pages/consumer/components/ConsumerForm/index.tsx index ff1db942..16878ffb 100644 --- a/frontend/src/pages/consumer/components/ConsumerForm/index.tsx +++ b/frontend/src/pages/consumer/components/ConsumerForm/index.tsx @@ -1,20 +1,24 @@ -import { ServiceSourceTypes } from '@/interfaces/service-source'; -import { Form, Input, Select, Tabs } from 'antd'; -import TextArea from 'antd/lib/input/TextArea'; +import { Form, Input, Select, Tabs, Button, Row, Col } from 'antd'; import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; import { useTranslation } from 'react-i18next'; -const { Option } = Select; - const ConsumerForm: React.FC = forwardRef((props, ref) => { const { t } = useTranslation(); const { value } = props; const [form] = Form.useForm(); const [activeTabKey, setActiveTabKey] = useState(''); + const [creSource, setCreSource] = useState('BEARER'); useEffect(() => { - setActiveTabKey('key-auth'); - form.resetFields(); + const { name = "", credentials = [] } = value; + if (name && credentials.length) { + setActiveTabKey(credentials[0].type); + setCreSource(credentials[0].source); + form.setFieldsValue({ name, credentials }); + } else { + form.resetFields(); + setActiveTabKey('key-auth'); + } }, [value]); useImperativeHandle(ref, () => ({ @@ -23,10 +27,33 @@ const ConsumerForm: React.FC = forwardRef((props, ref) => { }, handleSubmit: async () => { const values = await form.validateFields(); - return values; + if (activeTabKey === "key-auth") { + values["credentials"][0]["type"] = activeTabKey; + return values; + } + + return activeTabKey }, })); + const generateUUID = () => { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + let r = Math.random() * 16 | 0; + let v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + }; + + const setAuthValue = () => { + const uuid = generateUUID(); + changeCredentials({ value: uuid }); + } + + const changeCredentials = (params) => { + const oldData = form.getFieldValue("credentials"); + form.setFieldsValue({ credentials: [{ ...oldData[0], ...params }] }); + } + return ( { placeholder={t('consumer.consumerForm.namePlaceholder')} /> +
{t("consumer.columns.credentialTypes")}
setActiveTabKey(key)} + size="small" items={[ { label: 'Key Auth', @@ -67,18 +96,98 @@ const ConsumerForm: React.FC = forwardRef((props, ref) => { label: 'OAuth2', key: 'oauth2', children: ( - <> + <>{t("consumer.underDevelopment")} ), }, { label: 'JWT', key: 'jwt-auth', children: ( - <> + <>{t("consumer.underDevelopment")} ), }, ]} /> + { + activeTabKey === "key-auth" ? + + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }) => ( + <> + +
+ + + + + + {/* 随机生成 */} + + + + + {/* 令牌来源 */} + + + + + { + creSource === "HEADER" ? + // Header 名称 + + + + : null + } + + { + // 参数名称 + creSource === "QUERY" ? + + + + : null + } + + ))} + ) + } + + : null + } ); }); diff --git a/frontend/src/pages/consumer/index.tsx b/frontend/src/pages/consumer/index.tsx index c0eda442..1199aca9 100644 --- a/frontend/src/pages/consumer/index.tsx +++ b/frontend/src/pages/consumer/index.tsx @@ -5,7 +5,7 @@ import { addConsumer, deleteConsumer, getConsumers, updateConsumer } from '@/ser import { ExclamationCircleOutlined, RedoOutlined } from '@ant-design/icons'; import { PageContainer } from '@ant-design/pro-layout'; import { useRequest } from 'ahooks'; -import { Button, Col, Drawer, Form, Modal, Row, Space, Table, Tag } from 'antd'; +import { Button, Col, Drawer, Form, message, Modal, Row, Space, Table, Tag } from 'antd'; import React, { useEffect, useRef, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import ConsumerForm from './components/ConsumerForm'; @@ -96,7 +96,7 @@ const ConsumerList: React.FC = () => { manual: true, onSuccess: (result) => { const consumers = (result || []) as Consumer[]; - consumers.push({ name: 'test', credentials: [{ type: 'key-auth' }, { type: 'oauth' }] }); + // consumers.push({ name: 'test', credentials: [{ type: 'key-auth' }, { type: 'oauth' }] }); consumers.sort((i1, i2) => { return i1.name.localeCompare(i2.name); }) @@ -121,11 +121,14 @@ const ConsumerList: React.FC = () => { const handleDrawerOK = async () => { try { const values: FormProps = formRef.current ? await formRef.current.handleSubmit() : {} as FormProps; + if(!values.name && values) { + return message.info(values + t("consumer.underDevelopment")); + }; if (currentConsumer) { await updateConsumer({ version: currentConsumer.version, ...values } as Consumer); } else { - await addConsumer(values as Consumer); + await addConsumer({ ...values, version: 0 } as Consumer); } setOpenDrawer(false); From 0cfc6d6c8e10924a118d6ad68c65bd83486ae767 Mon Sep 17 00:00:00 2001 From: xpf <965732851@qq.com> Date: Thu, 12 Dec 2024 17:11:04 +0800 Subject: [PATCH 12/17] fix: value is null --- .../consumer/components/ConsumerForm/index.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/consumer/components/ConsumerForm/index.tsx b/frontend/src/pages/consumer/components/ConsumerForm/index.tsx index 16878ffb..40dc035a 100644 --- a/frontend/src/pages/consumer/components/ConsumerForm/index.tsx +++ b/frontend/src/pages/consumer/components/ConsumerForm/index.tsx @@ -10,11 +10,16 @@ const ConsumerForm: React.FC = forwardRef((props, ref) => { const [creSource, setCreSource] = useState('BEARER'); useEffect(() => { - const { name = "", credentials = [] } = value; - if (name && credentials.length) { - setActiveTabKey(credentials[0].type); - setCreSource(credentials[0].source); - form.setFieldsValue({ name, credentials }); + if (value) { + const { name = "", credentials = [] } = value; + if (name && credentials.length) { + setActiveTabKey(credentials[0].type); + setCreSource(credentials[0].source); + form.setFieldsValue({ name, credentials }); + } else { + form.resetFields(); + setActiveTabKey('key-auth'); + } } else { form.resetFields(); setActiveTabKey('key-auth'); From b0033ec54f29d74b90cde45f84dfe8472c77b10c Mon Sep 17 00:00:00 2001 From: xpf <965732851@qq.com> Date: Thu, 12 Dec 2024 17:56:45 +0800 Subject: [PATCH 13/17] fix: Delete close pop-up window --- frontend/src/pages/consumer/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/consumer/index.tsx b/frontend/src/pages/consumer/index.tsx index 1199aca9..8d0f65ed 100644 --- a/frontend/src/pages/consumer/index.tsx +++ b/frontend/src/pages/consumer/index.tsx @@ -152,7 +152,9 @@ const ConsumerList: React.FC = () => { const handleModalOk = async () => { setConfirmLoading(true); - await deleteConsumer(currentConsumer.name); + try { + await deleteConsumer(currentConsumer.name); + } catch (error) {} setConfirmLoading(false); setOpenModal(false); refresh(); From fe12db03ece16f1c2325c51aee7d90077a0a6f01 Mon Sep 17 00:00:00 2001 From: xpf <965732851@qq.com> Date: Thu, 12 Dec 2024 18:13:48 +0800 Subject: [PATCH 14/17] feat: add tips --- frontend/src/locales/en-US/translation.json | 8 +++++++- frontend/src/locales/zh-CN/translation.json | 8 +++++++- frontend/src/pages/consumer/index.tsx | 16 ++++++++++------ 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/frontend/src/locales/en-US/translation.json b/frontend/src/locales/en-US/translation.json index 156e4e26..b89111bf 100644 --- a/frontend/src/locales/en-US/translation.json +++ b/frontend/src/locales/en-US/translation.json @@ -84,7 +84,13 @@ "underDevelopment":"Under development, stay tuned.", "randomGeneration":"random generation", "headerName":"Header name", - "paramsName":"parameter name" + "paramsName":"parameter name", + "deleteSuccess": "delete success", + "deleteFailed": "delete failed", + "creaetSuccess": "creaet success", + "creatFailed": "creat failed", + "editSuccess": "edit successfully", + "editFailed": "edit failed" }, "domain": { "columns": { diff --git a/frontend/src/locales/zh-CN/translation.json b/frontend/src/locales/zh-CN/translation.json index 3afb1924..cabd9f4a 100644 --- a/frontend/src/locales/zh-CN/translation.json +++ b/frontend/src/locales/zh-CN/translation.json @@ -88,7 +88,13 @@ "underDevelopment":"开发中,敬请期待", "randomGeneration":"随机生成", "headerName":"Header 名称", - "paramsName":"参数名称" + "paramsName":"参数名称", + "deleteSuccess": "删除成功", + "deleteFailed": "删除失败", + "creaetSuccess": "创建成功", + "creatFailed": "创建失败", + "editSuccess": "编辑成功", + "editFailed": "编辑失败" }, "domain": { "columns": { diff --git a/frontend/src/pages/consumer/index.tsx b/frontend/src/pages/consumer/index.tsx index 8d0f65ed..4df11ab0 100644 --- a/frontend/src/pages/consumer/index.tsx +++ b/frontend/src/pages/consumer/index.tsx @@ -125,12 +125,15 @@ const ConsumerList: React.FC = () => { return message.info(values + t("consumer.underDevelopment")); }; - if (currentConsumer) { - await updateConsumer({ version: currentConsumer.version, ...values } as Consumer); - } else { - await addConsumer({ ...values, version: 0 } as Consumer); - } - + try { + if (currentConsumer) { + await updateConsumer({ version: currentConsumer.version, ...values } as Consumer); + message.success(t("consumer.editSuccess")); + } else { + await addConsumer({ ...values, version: 0 } as Consumer); + message.success(t("consumer.creaetSuccess")); + } + } catch(error) {} setOpenDrawer(false); formRef.current && formRef.current.reset(); refresh(); @@ -154,6 +157,7 @@ const ConsumerList: React.FC = () => { setConfirmLoading(true); try { await deleteConsumer(currentConsumer.name); + message.success(t("consumer.deleteSuccess")); } catch (error) {} setConfirmLoading(false); setOpenModal(false); From 81526636dc3c64dcf038ac4e50ba055f703e5fed Mon Sep 17 00:00:00 2001 From: xpf <965732851@qq.com> Date: Thu, 12 Dec 2024 18:16:18 +0800 Subject: [PATCH 15/17] fix: delet log --- frontend/src/pages/ai/components/RouteForm/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/ai/components/RouteForm/index.tsx b/frontend/src/pages/ai/components/RouteForm/index.tsx index b4de80b7..1b7f096a 100644 --- a/frontend/src/pages/ai/components/RouteForm/index.tsx +++ b/frontend/src/pages/ai/components/RouteForm/index.tsx @@ -176,7 +176,7 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { if (authConfig_enabled) { payload['authConfig']['allowedConsumers'] = authConfig_allowedConsumers ? [authConfig_allowedConsumers] : []; } - console.log('payload', payload) + return payload; }, })); From 85acdd1d2397cb2c32f17463242f9427305cad65 Mon Sep 17 00:00:00 2001 From: xpf <965732851@qq.com> Date: Tue, 17 Dec 2024 10:15:19 +0800 Subject: [PATCH 16/17] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4Ai=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/locales/en-US/translation.json | 2 + frontend/src/locales/zh-CN/translation.json | 2 + .../ai/components/ProviderForm/index.tsx | 39 +- .../pages/ai/components/RouteForm/const.tsx | 430 ++++++++++++++++++ .../pages/ai/components/RouteForm/index.tsx | 239 +++++----- .../components/ConsumerForm/index.tsx | 4 +- 6 files changed, 581 insertions(+), 135 deletions(-) create mode 100644 frontend/src/pages/ai/components/RouteForm/const.tsx diff --git a/frontend/src/locales/en-US/translation.json b/frontend/src/locales/en-US/translation.json index b89111bf..a0756d46 100644 --- a/frontend/src/locales/en-US/translation.json +++ b/frontend/src/locales/en-US/translation.json @@ -211,6 +211,8 @@ "modelProportion": "Proportionally", "modelName": "By model name", "addTargetAIservice": "Add target AI service", + "addTargetServer": "Add", + "operation": "Operation", "modelMatchingType": "Model matching type", "pleaseSelect": "Please select", "pleaseEnter": "Please enter", diff --git a/frontend/src/locales/zh-CN/translation.json b/frontend/src/locales/zh-CN/translation.json index cabd9f4a..c9c76e5f 100644 --- a/frontend/src/locales/zh-CN/translation.json +++ b/frontend/src/locales/zh-CN/translation.json @@ -216,6 +216,8 @@ "modelProportion": "按比例", "modelName": "按模型名称", "addTargetAIservice": "添加目标AI服务", + "addTargetServer": "添加", + "operation": "操作", "modelMatchingType":"模型匹配方式", "pleaseSelect":"请选择", "pleaseEnter":"请输入", diff --git a/frontend/src/pages/ai/components/ProviderForm/index.tsx b/frontend/src/pages/ai/components/ProviderForm/index.tsx index 86ae994d..f80eda60 100644 --- a/frontend/src/pages/ai/components/ProviderForm/index.tsx +++ b/frontend/src/pages/ai/components/ProviderForm/index.tsx @@ -28,7 +28,7 @@ const ProviderForm: React.FC = forwardRef((props: { value: any }, ref) => { type, protocol, tokens, - modelMapping = {}, + // modelMapping = {}, tokenFailoverConfig = {}, } = props.value; const { @@ -45,7 +45,7 @@ const ProviderForm: React.FC = forwardRef((props: { value: any }, ref) => { type, protocol, tokens, - modelMapping: getModelText(modelMapping), + // modelMapping: getModelText(modelMapping), enabled, failureThreshold, successThreshold, @@ -73,7 +73,7 @@ const ProviderForm: React.FC = forwardRef((props: { value: any }, ref) => { tokens: values.tokens, version: 0, // 资源版本号。进行创建或强制更新操作时需设置为 0。 / 1 表示强制更新 protocol: values.protocol, - modelMapping: getModelMapping(values.modelMapping), + // modelMapping: getModelMapping(values.modelMapping), tokenFailoverConfig: { enabled: values.enabled, }, @@ -108,13 +108,13 @@ const ProviderForm: React.FC = forwardRef((props: { value: any }, ref) => { } }; - const getModelText = (text) => { - try { - return Object.entries(text).map(([key, value]) => `${key}=${value}`).join('\n'); - } catch (err) { - return JSON.stringify(err) - } - }; + // const getModelText = (text) => { + // try { + // return Object.entries(text).map(([key, value]) => `${key}=${value}`).join('\n'); + // } catch (err) { + // return JSON.stringify(err) + // } + // }; return ( { noStyle > {/* 删除按钮 */} - {fields.length > 1 ? - (
(), - }, - { // "模型匹配方式", - title: t("llmProvider.modelMatchingType"), - dataIndex: 'matchValue', - render: () => ( +
+
+
+ + + + {/* 模型匹配方式 */} + {/* 模型名称 */} + {fields.length > 1 ? : null}{/* 操作 */} + + + + {fields.map(({ key, name, ...restField }, index) => ( + + + + + {fields.length > 1 ? + : null} + + ))} + +
Key{t("llmProvider.modelMatchingType")}{t("llmProvider.modelNames")}{t("llmProvider.operation")}
+ + - ), - }, - { // "模型名称", - title: t("llmProvider.modelNames"), - dataIndex: 'matchType', - render: () => ( + + - ), - }, - ]} - bordered={false} - pagination={false} - /> - {/* 目标AI服务 */} - - - - ) - })} + + + {index ? remove(name)} /> : null} +
+ + + +
+ +
) }
+ + + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }, index) => ( + + + + ))} + + )} + : <> - {/* 目标AI服务 upstreams */} { name={[name, 'provider']} rules={[{ required: true, message: t('llmProvider.providerForm.placeholder.aiName') }]} >{/* 服务名称 */} - setProviderIndex(text)} + > { llmList.map((item) => { const selectArr = form.getFieldValue('upstreams').map(i => i && i.provider) || []; return ( {item.name} - - ) - }) - } + ) + })} - {/* 请求比例 */} + >{/* 请求比例 */} { /> - {/* 模型名称 */} - - + {/* 模型名称 */} + setModelName(text)} + filterOption={(inputValue, option: any) => { + return option.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1 + }} + allowClear + placeholder={t("llmProvider.pleaseEnter")} + /> - {index ? remove(name)} /> : null} ))} - +
- +
) }} @@ -397,14 +424,15 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { } {/* 降级服务 */} + style={{ marginBottom: 0 }} + > { setFallbackConfigEnabled(e) - form.resetFields(["fallbackConfig_upstreams", "fallbackConfig_strategy"]) + form.resetFields(["fallbackConfig_upstreams"]) }} /> @@ -412,15 +440,12 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { {fallbackConfig_enabled ? <>
- {/* 降级服务列表 */} - {t('llmProvider.providerForm.label.routeStrategy1')} - {t('llmProvider.providerForm.label.routeStrategy')} - - : null } - {/* 请求认证设置 */} { setAuthConfigEnabled(e) diff --git a/frontend/src/pages/consumer/components/ConsumerForm/index.tsx b/frontend/src/pages/consumer/components/ConsumerForm/index.tsx index 40dc035a..2545f3fd 100644 --- a/frontend/src/pages/consumer/components/ConsumerForm/index.tsx +++ b/frontend/src/pages/consumer/components/ConsumerForm/index.tsx @@ -22,6 +22,8 @@ const ConsumerForm: React.FC = forwardRef((props, ref) => { } } else { form.resetFields(); + setActiveTabKey(''); + setCreSource('BEARER'); setActiveTabKey('key-auth'); } }, [value]); @@ -127,7 +129,7 @@ const ConsumerForm: React.FC = forwardRef((props, ref) => { name={[name, 'value']} rules={[{ required: true, message: t("consumer.consumerForm.pleaseEnter") }]} > - + From 6640c176f31e910f9d59a06eb330d1c3dc6f8512 Mon Sep 17 00:00:00 2001 From: xpf <965732851@qq.com> Date: Tue, 17 Dec 2024 14:33:49 +0800 Subject: [PATCH 17/17] feat: Optimize model selection --- .../pages/ai/components/RouteForm/index.tsx | 64 ++++++++++++++----- frontend/src/pages/ai/route.tsx | 10 ++- 2 files changed, 55 insertions(+), 19 deletions(-) diff --git a/frontend/src/pages/ai/components/RouteForm/index.tsx b/frontend/src/pages/ai/components/RouteForm/index.tsx index ebdcdd6e..5fa3c4d4 100644 --- a/frontend/src/pages/ai/components/RouteForm/index.tsx +++ b/frontend/src/pages/ai/components/RouteForm/index.tsx @@ -70,14 +70,22 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { setAuthConfigEnabled(_authConfig_enabled); setFallbackConfigEnabled(_fallbackConfig_enabled); + const initfallback = { fallbackConfig_enabled: _fallbackConfig_enabled }; + if (_fallbackConfig_enabled && value?.fallbackConfig?.upstreams) { + initfallback['fallbackConfig_upstreams'] = value?.fallbackConfig?.upstreams?.[0]?.provider; + try { + initfallback['fallbackConfig_modelNames'] = Object.keys(value?.fallbackConfig?.upstreams?.[0]?.modelMapping)[0]; + } catch (err) { + initfallback['fallbackConfig_modelNames'] = ''; + } + } const initValues = { name, domains: domains?.length ? domains[0] : [], upstreams, authConfig_enabled: _authConfig_enabled, authConfig_allowedConsumers: value?.authConfig?.allowedConsumers || "", - fallbackConfig_enabled: _fallbackConfig_enabled, - fallbackConfig_upstreams: value?.fallbackConfig?.upstreams ? value?.fallbackConfig?.upstreams[0].provider : undefined, + ...initfallback, }; if (modelPredicates) { @@ -134,6 +142,7 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { domains, fallbackConfig_upstreams = '', authConfig_allowedConsumers = '', + fallbackConfig_modelNames = '', modelPredicates = [], } = values; @@ -161,7 +170,12 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { payload["modelPredicates"] = modelPredicates.map(({ matchType, matchValue }) => ({ matchType, matchValue })); } if (fallbackConfig_enabled) { - payload['fallbackConfig']['upstreams'] = fallbackConfig_upstreams ? [{ provider: fallbackConfig_upstreams }] : []; + const _upstreams = { + provider: fallbackConfig_upstreams, + modelMapping: {}, + }; + _upstreams["modelMapping"][fallbackConfig_modelNames] = fallbackConfig_upstreams; + payload['fallbackConfig']['upstreams'] = [_upstreams]; payload['fallbackConfig']['strategy'] = "SEQ"; } if (authConfig_enabled) { @@ -171,23 +185,25 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => { }, })); + const getOptionsForAi = (providerName) => { + try { // 通过 【服务名称】 来筛查满足 【目标模型 预定义】 的下拉选项 + const _list = aiModelproviders.filter(item => item.value.toUpperCase().indexOf(providerName.toUpperCase()) !== -1); + if (_list.length) { + const _filterList = _list.map(item => item.targetModelList); + return _filterList.flatMap(item => item) + } + return []; + } catch (error) { return []; } + } + const getOptions = (index) => { if (modelService === "ModelName") { return [] } - try { const _upstreams = form.getFieldValue("upstreams"); // 查询 ui 全部Ai服务 - if (_upstreams[index]) { - const _upstreams_modelMapping = _upstreams[index]; // 通过传递的 index 筛查出当前Ai服务的【服务名称】。 - if (_upstreams_modelMapping && _upstreams_modelMapping.provider) { // 通过 【服务名称】 来筛查满足 【目标模型 预定义】 的下拉选项 - const _list = aiModelproviders.filter(item => item.value.toUpperCase().indexOf(_upstreams_modelMapping.provider.toUpperCase()) !== -1); - if (_list.length) { - const _filterList = _list.map(item => item.targetModelList); - return _filterList.flatMap(item => item) - } - return []; - } + if (_upstreams[index]) { // 通过传递的 index 筛查出当前Ai服务的【服务名称】。 + if (_upstreams[index] && _upstreams[index].provider) return getOptionsForAi(_upstreams[index].provider); } } catch (err) { return []; } return []; @@ -442,16 +458,30 @@ const ConsumerForm: React.FC = forwardRef((props: { value: any }, ref) => {
- setProviderIndex(text)}> { llmList.map((item) => ( {item.name} ))} - + {/* 模型名称 */} + setModelName(text)} + filterOption={(inputValue, option: any) => option.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1} + allowClear + placeholder={t("llmProvider.pleaseEnter")} + /> +
: null diff --git a/frontend/src/pages/ai/route.tsx b/frontend/src/pages/ai/route.tsx index 2e3b81c5..8815a9a7 100644 --- a/frontend/src/pages/ai/route.tsx +++ b/frontend/src/pages/ai/route.tsx @@ -97,6 +97,7 @@ const AiRouteList: React.FC = () => { const [dataSource, setDataSource] = useState([]); const [currentAiRoute, setCurrentAiRoute] = useState(); const [openDrawer, setOpenDrawer] = useState(false); + const [loadingapi, setLoading] = useState(false); const [openModal, setOpenModal] = useState(false); const [confirmLoading, setConfirmLoading] = useState(false); const [useDrawer, setUseDrawer] = useState(false) @@ -137,8 +138,12 @@ const AiRouteList: React.FC = () => { }; const handleDrawerOK = async () => { + setLoading(true); const values = formRef.current ? await formRef.current.handleSubmit() : {}; - if (!values) return false; + if (!values) { + setLoading(false); + return false; + } if (currentAiRoute) { const params: AiRoute = { version: currentAiRoute.version, ...values }; @@ -147,6 +152,7 @@ const AiRouteList: React.FC = () => { await addAiRoute(values as AiRoute); } + setLoading(false); setOpenDrawer(false); formRef.current && formRef.current.reset(); refresh(); @@ -231,7 +237,7 @@ const AiRouteList: React.FC = () => { extra={ -