mirror of
https://github.com/APIParkLab/APIPark.git
synced 2026-06-14 20:41:15 +08:00
feat: add llm status manage
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user