import { useEffect, useState } from 'react' import { Button, Divider, Drawer, Form, Input, InputNumber, message, Popconfirm, Select, Space, Switch, Table, Tag, Tooltip, } from 'antd' import { ApiOutlined, CheckCircleOutlined, DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined, } from '@ant-design/icons' import { modelConfigsApi, type ModelCapability, type ModelConfig, type ModelConfigPayload, type ModelModality, type ModelProtocol, } from '../api' import PageWrapper from '../components/PageWrapper' import { formatDateTime } from '../utils/date' import { notifyModelConfigsChanged } from '../utils/modelConfigEvents' const capabilityLabels: Record = { chat: '对话', embedding: '向量', moderation: '内容审核', } const capabilityColors: Record = { chat: 'blue', embedding: 'green', moderation: 'orange', } const modalityLabels: Record = { text: '文本', image: '图像', audio: '音频', video: '视频', } const modalityColors: Record = { text: 'default', image: 'cyan', audio: 'magenta', video: 'purple', } const modalityOptions = Object.entries(modalityLabels).map(([value, label]) => ({ value, label })) const featureLabels: Array<[keyof Pick< ModelConfig, 'supports_streaming' | 'supports_tool_calling' | 'supports_structured_output' | 'supports_reasoning' >, string]> = [ ['supports_streaming', '流式'], ['supports_tool_calling', '工具调用'], ['supports_structured_output', '结构化输出'], ['supports_reasoning', '推理'], ] const formatTokens = (value: number | null) => value ? new Intl.NumberFormat('zh-CN').format(value) : '-' const protocolOptions: Record = { openai_compatible: { label: 'OpenAI 兼容', capabilities: ['chat', 'embedding', 'moderation'], endpointPlaceholder: 'https://api.example.com/v1/chat/completions', }, anthropic: { label: 'Anthropic Messages', capabilities: ['chat'], endpointPlaceholder: 'https://api.anthropic.com/v1/messages', }, google_gemini: { label: 'Google Gemini', capabilities: ['chat'], endpointPlaceholder: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent', }, dashscope: { label: 'DashScope 原生', capabilities: ['chat'], endpointPlaceholder: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation', }, } export default function ModelConfigsPage() { const [configs, setConfigs] = useState([]) const [loading, setLoading] = useState(false) const [drawerOpen, setDrawerOpen] = useState(false) const [editing, setEditing] = useState(null) const [testingId, setTestingId] = useState(null) const [capability, setCapability] = useState() const [enabled, setEnabled] = useState() const [form] = Form.useForm() const selectedProtocol = Form.useWatch('provider', form) || 'openai_compatible' const selectedProtocolMeta = protocolOptions[selectedProtocol] const load = async () => { setLoading(true) try { const response = await modelConfigsApi.list({ capability, enabled }) setConfigs(response.data) } finally { setLoading(false) } } useEffect(() => { load() }, [capability, enabled]) const openCreate = () => { setEditing(null) form.resetFields() form.setFieldsValue({ provider: 'openai_compatible', capability: 'chat', vendor_name: '', input_modalities: ['text'], output_modalities: ['text'], context_window: null, max_output_tokens: null, supports_streaming: false, supports_tool_calling: false, supports_structured_output: false, supports_reasoning: false, region: '', documentation_url: null, enabled: true, is_default: false, description: '', }) setDrawerOpen(true) } const openEdit = (config: ModelConfig) => { setEditing(config) form.setFieldsValue({ name: config.name, provider: config.provider, capability: config.capability, endpoint_url: config.endpoint_url, model_name: config.model_name, vendor_name: config.vendor_name, input_modalities: config.input_modalities, output_modalities: config.output_modalities, context_window: config.context_window, max_output_tokens: config.max_output_tokens, supports_streaming: config.supports_streaming, supports_tool_calling: config.supports_tool_calling, supports_structured_output: config.supports_structured_output, supports_reasoning: config.supports_reasoning, region: config.region, documentation_url: config.documentation_url, api_key: null, clear_api_key: false, enabled: config.enabled, is_default: config.is_default, description: config.description, }) setDrawerOpen(true) } const changeProtocol = (provider: ModelProtocol) => { const supportedCapabilities = protocolOptions[provider].capabilities if (!supportedCapabilities.includes(form.getFieldValue('capability'))) { form.setFieldValue('capability', supportedCapabilities[0]) } } const submit = async () => { const values = await form.validateFields() const payload: ModelConfigPayload = { ...values, api_key: values.api_key || null, model_name: values.model_name || null, vendor_name: values.vendor_name || '', region: values.region || '', documentation_url: values.documentation_url || null, description: values.description || '', } if (editing) { await modelConfigsApi.update(editing.id, payload) message.success('模型配置已更新') } else { await modelConfigsApi.create(payload) message.success('模型配置已创建') } notifyModelConfigsChanged() setDrawerOpen(false) load() } const testConnection = async (config: ModelConfig) => { setTestingId(config.id) try { const response = await modelConfigsApi.test(config.id) if (response.data.ok) message.success(`${config.name} 连接成功`) else message.warning(response.data.message) } finally { setTestingId(null) } } const remove = async (config: ModelConfig) => { await modelConfigsApi.delete(config.id) message.success('模型配置已删除') notifyModelConfigsChanged() load() } const columns = [ { title: '配置名称', dataIndex: 'name', key: 'name', width: 220, render: (value: string, record: ModelConfig) => (
{value} {record.is_default && 默认} {(record.vendor_name || record.region) && (
{[record.vendor_name, record.region].filter(Boolean).join(' · ')}
)}
), }, { title: '协议', dataIndex: 'provider', key: 'provider', width: 150, render: (value: ModelProtocol) => {protocolOptions[value]?.label || value}, }, { title: '能力', dataIndex: 'capability', key: 'capability', width: 110, render: (value: ModelCapability) => {capabilityLabels[value]}, }, { title: '输入 / 输出', key: 'modalities', width: 230, render: (_: unknown, record: ModelConfig) => ( 输入 {record.input_modalities.map((value) => ( {modalityLabels[value]} ))} 输出 {record.output_modalities.map((value) => ( {modalityLabels[value]} ))} ), }, { title: '模型', dataIndex: 'model_name', key: 'model_name', width: 180, ellipsis: true, render: (value: string | null) => value || '-', }, { title: '规格', key: 'limits', width: 170, render: (_: unknown, record: ModelConfig) => (
上下文 {formatTokens(record.context_window)}
最大输出 {formatTokens(record.max_output_tokens)}
), }, { title: '特性', key: 'features', width: 210, render: (_: unknown, record: ModelConfig) => { const enabledFeatures = featureLabels.filter(([key]) => record[key]) return enabledFeatures.length ? ( {enabledFeatures.map(([key, label]) => {label})} ) : '-' }, }, { title: 'Endpoint', dataIndex: 'endpoint_url', key: 'endpoint_url', width: 260, ellipsis: true }, { title: '凭据', dataIndex: 'has_api_key', key: 'has_api_key', width: 90, render: (value: boolean) => value ? 已配置 : 无, }, { title: '状态', dataIndex: 'enabled', key: 'enabled', width: 90, render: (value: boolean) => value ? 启用 : 停用, }, { title: '更新时间', dataIndex: 'updated_at', key: 'updated_at', width: 170, render: (value: string | null) => formatDateTime(value), }, { title: '操作', key: 'actions', width: 145, fixed: 'right' as const, render: (_: unknown, record: ModelConfig) => ( } >
`共 ${total} 个` }} scroll={{ x: 1800, y: 'calc(100vh - 200px)' }} style={{ height: '100%' }} /> setDrawerOpen(false)} extra={} >
基础信息
模型特性
连接设置
({ value, label: capabilityLabels[value] }))} />
{() => form.getFieldValue('capability') !== 'moderation' && ( )} {editing?.has_api_key && ( )}
) }