feat: add llm status manage

This commit is contained in:
scarqin
2024-12-31 12:01:42 +08:00
parent 9fc8234eaf
commit 0dbf9c907d
4 changed files with 88 additions and 47 deletions
@@ -22,17 +22,17 @@ import { ModelCardNode } from './components/ModelCardNode'
import { ServiceCardNode } from './components/NodeComponents'
import { LAYOUT } from './constants'
import './styles.css'
import { ModelData } from './types'
import { ModelListData } from './types'
export type ApiResponse = BasicResponse<{
backup: {
id: string
name: string
}
providers: ModelData[]
providers: ModelListData[]
}>
const calculateNodePositions = (models: ModelData[], startY = LAYOUT.NODE_START_Y, gap = LAYOUT.NODE_GAP) => {
const calculateNodePositions = (models: ModelListData[], startY = LAYOUT.NODE_START_Y, gap = LAYOUT.NODE_GAP) => {
return models.reduce(
(acc, model, index) => {
const y = startY + index * gap
@@ -63,7 +63,7 @@ const edgeTypes: EdgeTypes = {
}
const AIFlowChart = () => {
const [modelData, setModelData] = useState<ModelData[]>([])
const [modelData, setModelData] = useState<ModelListData[]>([])
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
const { fetchData } = useFetch()
@@ -71,7 +71,8 @@ const AIFlowChart = () => {
useEffect(() => {
fetchData<ApiResponse>('ai/providers/configured', {
method: 'GET'
method: 'GET',
eoTransformKeys: ['default_llm']
}).then((response) => {
const mockApiResponse: ApiResponse = response as ApiResponse
setModelData(mockApiResponse.data.providers)
@@ -84,7 +85,6 @@ const AIFlowChart = () => {
const positions = calculateNodePositions(modelData)
// subtract 5 to make sure the service node is aligned with the top model node
const serviceY = positions[modelData[0].id].y - 5
const newNodes = [
{
id: 'apiService',
@@ -100,9 +100,9 @@ const AIFlowChart = () => {
type: 'modelCard',
position: positions[model.id],
data: {
title: model.name,
name: model.name,
status: model.status,
defaultLlm: model.default_llm,
defaultLlm: model.defaultLlm,
logo: model.logo,
id: model.id
}
@@ -3,9 +3,9 @@ import { Codebox } from '@common/components/postcat/api/Codebox'
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useFetch } from '@common/hooks/http'
import { $t } from '@common/locales'
import { App, Form, InputNumber, Select, Tag, Tooltip } from 'antd'
import { App, Form, InputNumber, Select, Switch, Tag, Tooltip } from 'antd'
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
import { AiProviderConfig, AiProviderLlmsItems } from './AiSettingList'
import { AiProviderConfig, AiProviderLlmsItems, ModelDetailData } from './types'
export type AiSettingModalContentProps = {
entity: AiProviderConfig & { defaultLlm: string }
@@ -16,12 +16,6 @@ export type AiSettingModalContentHandle = {
save: () => Promise<boolean | string>
}
type AiSettingModalContentField = {
config: string
defaultLlm: string
priority: number
}
const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingModalContentProps>((props, ref) => {
const [form] = Form.useForm()
const { message } = App.useApp()
@@ -55,13 +49,15 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
form.setFieldsValue({
defaultLlm: entity.defaultLlm,
config: entity!.config ? JSON.stringify(JSON.parse(entity!.config), null, 2) : '',
priority: entity.priority || 1
priority: entity.priority || 1,
enable: entity.status === 'enabled'
})
} catch (e) {
form.setFieldsValue({
defaultLlm: entity.defaultLlm,
config: '',
priority: 1
priority: 1,
enable: true
})
}
}, [])
@@ -99,6 +95,13 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
})
}
const getTooltipText = (isChecked: boolean) => {
if (!isChecked) {
return '保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。'
}
return '保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。'
}
useImperativeHandle(ref, () => ({
save
}))
@@ -112,8 +115,9 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
className="flex flex-col mx-auto h-full"
name="aiServiceInsideRouterModalConfig"
autoComplete="off"
disabled={readOnly}
>
<Form.Item<AiSettingModalContentField> label={$t('默认模型')} name="defaultLlm" rules={[{ required: true }]}>
<Form.Item<ModelDetailData> label={$t('默认模型')} name="defaultLlm" rules={[{ required: true }]}>
<Select
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.select)}
@@ -130,7 +134,7 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
></Select>
</Form.Item>
<Form.Item<AiSettingModalContentField>
<Form.Item<ModelDetailData>
label={
<span className="flex items-center">
{$t('负载优先级')}
@@ -147,7 +151,7 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
{
validator: async (_, value) => {
if (value <= 0) {
throw new Error($t('优先级必须大于0'))
throw new Error($t('优先级必须大于 0'))
}
return Promise.resolve()
}
@@ -158,16 +162,42 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
<InputNumber className="w-INPUT_NORMAL" min={1} placeholder={$t('请输入优先级')} />
</Form.Item>
<Form.Item<AiSettingModalContentField> label={$t('参数')} name="config">
<Form.Item<ModelDetailData> label={$t('API Key(默认 Key')} name="config">
<Codebox
editorTheme="vs-dark"
readOnly={readOnly}
width="100%"
height="300px"
height="200px"
language="json"
enableToolbar={false}
/>
</Form.Item>
{entity.id && (
<Form.Item className="p-4 bg-white rounded-lg" label={$t('LLM 状态管理')}>
<div className="flex justify-between items-center">
<div>
<span className="text-gray-600"></span>
{entity.status === 'enabled' && <Tag color="success">{$t('正常')}</Tag>}
{entity.status === 'disabled' && <Tag color="warning">{$t('停用')}</Tag>}
{entity.status === 'abnormal' && <Tag color="error">{$t('异常')}</Tag>}
</div>
<Form.Item name="enable" valuePropName="checked" noStyle>
<Switch
checkedChildren={$t('启用')}
unCheckedChildren={$t('停用')}
onChange={(checked) => {
form.setFieldsValue({ enable: checked })
}}
/>
</Form.Item>
</div>
{(entity.status === 'enabled' && !form.getFieldValue('enable')) ||
(entity.status !== 'enabled' && form.getFieldValue('enable')) ? (
<div className="mt-2 text-sm text-gray-500">* {getTooltipText(form.getFieldValue('enable'))}</div>
) : null}
</Form.Item>
)}
</Form>
)
})
@@ -4,27 +4,34 @@ import { Handle, Position } from '@xyflow/react'
import { t } from 'i18next'
import React from 'react'
import { useAiSetting } from '../contexts/AiSettingContext'
import { AiSettingListItem, ModelStatus } from '../types'
import { AiSettingListItem, ModelDetailData, ModelStatus } from '../types'
interface ModelCardData {
title: string
status: ModelStatus
logo: string
defaultLlm: string
}
type ModelCardNodeData = ModelCardData & {
type ModelCardNodeData = ModelDetailData & {
id: string
position: { x: number; y: number }
}
export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) => {
const { title, status, defaultLlm, logo } = data
const { name, status, defaultLlm, logo } = data
const { openConfigModal } = useAiSetting()
const { aiConfigFlushed, setAiConfigFlushed } = useGlobalContext()
const getStatusIcon = (status: ModelStatus) => {
switch (status) {
case 'enabled':
return { icon: 'mdi:check-circle', color: 'text-green-500' }
case 'disabled':
return { icon: 'mdi:pause-circle', color: 'text-gray-400' }
case 'abnormal':
return { icon: 'mdi:alert-circle', color: 'text-red-500' }
}
}
const statusConfig = getStatusIcon(status)
return (
<div
className="node-card bg-white rounded-lg shadow-sm p-4 min-w-[280px] group"
className="node-card bg-white rounded-lg shadow-sm p-4 min-w-[280px] group"
style={{ border: '1px solid var(--border-color)' }}
>
<Handle type="target" position={Position.Left} />
@@ -32,17 +39,14 @@ export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) =
<div>
<div className="flex justify-between items-center">
<div className="flex gap-2 items-center">
<div className="flex flex-1 overflow-hidden items-center gap-[4px]">
<div className="flex flex-1 overflow-hidden items-center gap-[4px]">
<span
className=" flex items-center h-[22px] ai-setting-svg-container"
className="flex items-center h-[22px] ai-setting-svg-container"
dangerouslySetInnerHTML={{ __html: logo }}
></span>
</div>
<span className="text-base text-gray-900 max-w-[180px] truncate">{title}</span>
<Icon
icon={status === 'enable' ? 'mdi:check-circle' : 'mdi:close-circle'}
className={`text-xl ${status === 'enable' ? 'text-green-500' : 'text-red-500'}`}
/>
<span className="text-base text-gray-900 max-w-[180px] truncate">{name}</span>
<Icon icon={statusConfig.icon} className={`text-xl ${statusConfig.color}`} />
</div>
{/* Action buttons */}
@@ -1,4 +1,4 @@
export type ModelStatus = 'enable' | 'abnormal'|'disabled'
export type ModelStatus = 'enabled' | 'abnormal'|'disabled'
export type KeyStatus ='normal' | 'abnormal'|'disabled'
export interface KeyData {
@@ -7,18 +7,23 @@ export interface KeyData {
status: KeyStatus,
}
export interface ModelData {
export interface ModelListData {
id: string
name: string
logo: string
default_llm: string
defaultLlm: string
status: ModelStatus
api_count: number
key_count: number
keys: KeyData[]
}
export interface ModelDetailData extends ModelListData{
enable:boolean
config: string,
priority?: number
}
export type AiSettingListItem = {
name: string
id: string
@@ -46,10 +51,12 @@ export type AiProviderDefaultConfig = {
scopes: string[]
}
export type AiProviderConfig = {
export interface AiProviderConfig {
id: string
name: string
config?: string
config: string
getApikeyUrl: string
priority?: number
priority: number
enable: boolean
status: ModelStatus
}