chore: provider form test

This commit is contained in:
jialin
2026-02-03 18:21:08 +08:00
parent d86d061f97
commit e188d788b9
27 changed files with 448 additions and 317 deletions
+3
View File
@@ -27,6 +27,7 @@ const SealSelect: React.FC<
loading, loading,
footer, footer,
alwaysFocus = false, alwaysFocus = false,
styles,
...rest ...rest
} = props; } = props;
const intl = useIntl(); const intl = useIntl();
@@ -109,6 +110,8 @@ const SealSelect: React.FC<
> >
<BaseSelect <BaseSelect
{...rest} {...rest}
styles={styles}
placeholder={placeholder}
footer={footer} footer={footer}
ref={inputRef} ref={inputRef}
options={children ? null : _options} options={children ? null : _options}
+2 -1
View File
@@ -4,7 +4,8 @@ export enum PageAction {
CREATE = 'create', CREATE = 'create',
UPDATE = 'update', UPDATE = 'update',
VIEW = 'view', VIEW = 'view',
EDIT = 'edit' EDIT = 'edit',
COPY = 'copy'
} }
export const StatusColorMap: Record< export const StatusColorMap: Record<
+1 -1
View File
@@ -6,7 +6,7 @@ export interface DropDownItem {
iconfont?: boolean; iconfont?: boolean;
} }
export type PageActionType = 'create' | 'update' | 'view' | 'edit'; export type PageActionType = 'create' | 'update' | 'view' | 'edit' | 'copy';
export type StatusType = export type StatusType =
| 'error' | 'error'
+2 -1
View File
@@ -11,5 +11,6 @@ export default {
'providers.form.proxy.url': 'Proxy URL', 'providers.form.proxy.url': 'Proxy URL',
'providers.form.proxy.timeout': 'Proxy Timeout (seconds)', 'providers.form.proxy.timeout': 'Proxy Timeout (seconds)',
'providers.form.customConfig': 'Custom Configuration', 'providers.form.customConfig': 'Custom Configuration',
'providers.form.model.test': 'Test Connection' 'providers.form.model.test': 'Test',
'providers.form.model.test.tips': 'Test Connection'
}; };
+2 -1
View File
@@ -11,5 +11,6 @@ export default {
'providers.form.proxy.url': 'Proxy URL', 'providers.form.proxy.url': 'Proxy URL',
'providers.form.proxy.timeout': 'Proxy Timeout (seconds)', 'providers.form.proxy.timeout': 'Proxy Timeout (seconds)',
'providers.form.customConfig': 'Custom Configuration', 'providers.form.customConfig': 'Custom Configuration',
'providers.form.model.test': 'Test Connection' 'providers.form.model.test': 'Test',
'providers.form.model.test.tips': 'Test Connection'
}; };
+2 -1
View File
@@ -11,5 +11,6 @@ export default {
'providers.form.proxy.url': 'Proxy URL', 'providers.form.proxy.url': 'Proxy URL',
'providers.form.proxy.timeout': 'Proxy Timeout (seconds)', 'providers.form.proxy.timeout': 'Proxy Timeout (seconds)',
'providers.form.customConfig': 'Custom Configuration', 'providers.form.customConfig': 'Custom Configuration',
'providers.form.model.test': 'Test Connection' 'providers.form.model.test': 'Test',
'providers.form.model.test.tips': 'Test Connection'
}; };
+3 -2
View File
@@ -8,8 +8,9 @@ export default {
'providers.form.tokens.add': '添加 API 令牌', 'providers.form.tokens.add': '添加 API 令牌',
'providers.form.models.add': '添加模型', 'providers.form.models.add': '添加模型',
'providers.form.proxy.enable': '启用代理', 'providers.form.proxy.enable': '启用代理',
'providers.form.proxy.url': '代理 URL', 'providers.form.proxy.url': '代理地址',
'providers.form.proxy.timeout': '代理超时(秒)', 'providers.form.proxy.timeout': '代理超时(秒)',
'providers.form.customConfig': '自定义配置', 'providers.form.customConfig': '自定义配置',
'providers.form.model.test': '测试连接' 'providers.form.model.test': '测试',
'providers.form.model.test.tips': '测试连接'
}; };
+35 -6
View File
@@ -1,9 +1,11 @@
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import { FormData, MaasProviderItem } from '../config/types'; import { FormData, MaasProviderItem } from '../config/types';
export const MAAS_PROVIDERS_API = '/models'; export const MAAS_PROVIDERS_API = '/model-providers';
export const PROVIDER_MODELS_API = '/models'; export const GET_PROVIDER_MODELS_API = '/get-models';
export const TEST_PROVIDER_MODEL_API = '/test-model';
export async function queryMaasProviders( export async function queryMaasProviders(
params: Global.SearchParams, params: Global.SearchParams,
@@ -37,14 +39,41 @@ export async function deleteProvider(id: number) {
} }
export async function queryProviderModels( export async function queryProviderModels(
params: { id: string }, params: {
data: {
api_token: string;
config: {
type: string;
};
};
},
options?: any options?: any
) { ) {
return request<Global.BaseOption<string>[]>( return request<{ data: any[] }>(
`${MAAS_PROVIDERS_API}/${params.id}/models`, `${MAAS_PROVIDERS_API}${GET_PROVIDER_MODELS_API}`,
{ {
method: 'GET', method: 'post',
data: params.data,
cancelToken: options?.token cancelToken: options?.token
} }
); );
} }
export async function testProviderModel(
params: {
data: {
model_name: string;
api_token: string;
config: {
type: string;
};
};
},
options?: any
) {
return request<any>(`${MAAS_PROVIDERS_API}${TEST_PROVIDER_MODEL_API}`, {
method: 'post',
data: params.data,
cancelToken: options?.token
});
}
@@ -3,7 +3,6 @@ import FormDrawer from '@/pages/_components/form-drawer';
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { FormData, MaasProviderItem as ListItem } from '../config/types'; import { FormData, MaasProviderItem as ListItem } from '../config/types';
import { maasProviderType } from '../config';
import ProviderForm from '../forms'; import ProviderForm from '../forms';
type AddModalProps = { type AddModalProps = {
@@ -11,7 +10,6 @@ type AddModalProps = {
action: PageActionType; action: PageActionType;
open: boolean; open: boolean;
currentData?: ListItem; // Used when action is EDIT currentData?: ListItem; // Used when action is EDIT
provider: maasProviderType | null;
onOk: (values: FormData) => void; onOk: (values: FormData) => void;
onCancel: () => void; onCancel: () => void;
}; };
@@ -19,7 +17,6 @@ const AddProvider: React.FC<AddModalProps> = ({
title, title,
action, action,
open, open,
provider,
currentData, currentData,
onOk, onOk,
onCancel onCancel
@@ -30,7 +27,8 @@ const AddProvider: React.FC<AddModalProps> = ({
form.current?.submit(); form.current?.submit();
}; };
const handleOk = async (data: FormData) => { const handleOnFinish = async (data: FormData) => {
console.log('handleOnFinish', data);
onOk({ onOk({
...data ...data
}); });
@@ -53,7 +51,7 @@ const AddProvider: React.FC<AddModalProps> = ({
ref={form} ref={form}
action={action} action={action}
currentData={currentData} currentData={currentData}
onFinish={handleOk} onFinish={handleOnFinish}
/> />
</FormDrawer> </FormDrawer>
); );
@@ -1,5 +1,4 @@
import AutoTooltip from '@/components/auto-tooltip'; import AutoTooltip from '@/components/auto-tooltip';
import { modelCategoriesMap } from '@/pages/llmodels/config';
import { import {
CheckCircleOutlined, CheckCircleOutlined,
CloseCircleOutlined, CloseCircleOutlined,
@@ -8,56 +7,13 @@ import {
import { Flex, Tag } from 'antd'; import { Flex, Tag } from 'antd';
import React from 'react'; import React from 'react';
import { categoryConfig } from '../../_components/model-tag'; import { categoryConfig } from '../../_components/model-tag';
import { ProviderModel } from '../config/types';
interface ProviderModelProps { interface ProviderModelProps {
dataList: any[]; dataList: ProviderModel[];
provider: string;
providerId: number;
} }
const models: { const ProviderModels: React.FC<ProviderModelProps> = ({ dataList }) => {
name: string;
status: 'accessible' | 'inaccessible' | 'none';
type: string;
}[] = [
{
name: 'model-1',
status: 'accessible',
type: modelCategoriesMap.llm
},
{
name: 'model-2',
status: 'inaccessible',
type: modelCategoriesMap.embedding
},
{
name: 'model-3',
status: 'none',
type: modelCategoriesMap.image
},
{
name: 'model-4',
status: 'accessible',
type: modelCategoriesMap.llm
},
{
name: 'model-5',
status: 'inaccessible',
type: modelCategoriesMap.embedding
},
{
name: 'model-6model-6model-6model-6model-6model-6model-6',
status: 'none',
type: modelCategoriesMap.image
},
{
name: 'model-7',
status: 'accessible',
type: modelCategoriesMap.llm
}
];
const ProviderModels: React.FC<ProviderModelProps> = () => {
const iconsMap = { const iconsMap = {
accessible: <CheckCircleOutlined />, accessible: <CheckCircleOutlined />,
inaccessible: <CloseCircleOutlined />, inaccessible: <CloseCircleOutlined />,
@@ -66,10 +22,14 @@ const ProviderModels: React.FC<ProviderModelProps> = () => {
return ( return (
<div style={{ paddingInline: 16 }}> <div style={{ paddingInline: 16 }}>
<Flex gap="8px" wrap="wrap"> <Flex gap="8px" wrap="wrap">
{models.map((model) => ( {dataList.map((model) => (
<Tag <Tag
key={model.name} key={model.name}
icon={iconsMap[model.status]} icon={
model.accessible !== null
? iconsMap[model.accessible ? 'accessible' : 'inaccessible']
: iconsMap['none']
}
variant="outlined" variant="outlined"
styles={{ styles={{
root: { root: {
@@ -80,9 +40,9 @@ const ProviderModels: React.FC<ProviderModelProps> = () => {
} }
}} }}
color={ color={
model.status === 'accessible' model.accessible === true
? 'success' ? 'success'
: model.status === 'inaccessible' : model.accessible === false
? 'error' ? 'error'
: 'warning' : 'warning'
} }
@@ -92,7 +52,7 @@ const ProviderModels: React.FC<ProviderModelProps> = () => {
{model.name} {model.name}
</AutoTooltip> </AutoTooltip>
<span style={{ marginLeft: 8 }}> <span style={{ marginLeft: 8 }}>
{categoryConfig[model.type]?.icon} {categoryConfig[model.category]?.icon}
</span> </span>
</span> </span>
</Tag> </Tag>
@@ -1,8 +1,9 @@
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import { createContext, useContext } from 'react'; import { createContext, useContext } from 'react';
import { maasProviderType } from '.';
interface FormContextProps { interface FormContextProps {
providerType?: string; providerType?: maasProviderType;
action?: PageActionType; action?: PageActionType;
} }
-59
View File
@@ -1,59 +0,0 @@
import { MaasProviderItem } from '../config/types';
// mock data for MaasProviderItem 5 items
export const mockDataList: MaasProviderItem[] = [
{
id: 1,
name: 'Doubao',
state: 'Ready',
models: 5,
state_message: '',
provider: 'doubao',
api_tokens: ['token1', 'token2'],
proxy_url: 'http://proxy.example.com',
builtIn: false
},
{
id: 2,
models: 3,
name: 'Qwen',
state: 'inactive',
state_message: 'Provider is inactive',
provider: 'qwen',
proxy_url: 'http://proxy2.example.com',
builtIn: true
},
{
id: 3,
models: 8,
name: 'OpenAI',
state: 'ready',
state_message: '',
proxy_url: null,
api_tokens: ['tokenA'],
provider: 'openai',
builtIn: false
},
{
id: 4,
models: 2,
name: 'Deepseek',
state: 'ready',
state_message: '',
provider: 'deepseek',
proxy_url: null,
api_tokens: ['tokenX', 'tokenY'],
builtIn: false
},
{
id: 5,
models: 4,
name: 'Anthropic',
state: 'inactive',
state_message: 'Provider is inactive',
provider: 'claude',
proxy_url: {},
builtIn: true
}
];
+7 -2
View File
@@ -1,9 +1,14 @@
import { maasProviderType } from '.'; import { maasProviderType } from '.';
export interface ProviderModel {
name: string;
category: string;
accessible: boolean | null;
}
export interface FormData { export interface FormData {
name: string; name: string;
description: string; description: string;
api_tokens: string[]; api_tokens: string[];
models: ProviderModel[];
config: { config: {
type: maasProviderType; type: maasProviderType;
[key: string]: any; [key: string]: any;
@@ -15,7 +20,7 @@ export interface MaasProviderItem extends FormData {
updated_at: string; updated_at: string;
deleted_at: string; deleted_at: string;
timeout: number; timeout: number;
support_models: unknown[]; models: ProviderModel[];
proxy_url: string; proxy_url: string;
proxy_timeout: number; proxy_timeout: number;
builtin: boolean; builtin: boolean;
@@ -8,13 +8,16 @@ const AccessToken = () => {
const intl = useIntl(); const intl = useIntl();
const { getRuleMessage } = useAppUtils(); const { getRuleMessage } = useAppUtils();
const form = Form.useFormInstance<FormData>(); const form = Form.useFormInstance<FormData>();
const tokenList = Form.useWatch('api_tokens', form) || [];
const handleOnChange = (values: string[]) => {}; const handleOnChange = (values: string[]) => {
form.setFieldValue('api_tokens', values);
};
return ( return (
<> <>
<Form.Item <Form.Item
name="tokens" name="api_tokens"
rules={[ rules={[
{ {
required: true, required: true,
@@ -29,7 +32,7 @@ const AccessToken = () => {
required={true} required={true}
btnText={intl.formatMessage({ id: 'providers.form.tokens.add' })} btnText={intl.formatMessage({ id: 'providers.form.tokens.add' })}
label={intl.formatMessage({ id: 'providers.form.tokens.title' })} label={intl.formatMessage({ id: 'providers.form.tokens.title' })}
dataList={[]} dataList={tokenList}
onChange={handleOnChange} onChange={handleOnChange}
></ListInput> ></ListInput>
</Form.Item> </Form.Item>
@@ -4,14 +4,12 @@ import YamlEditor from '@/pages/_components/yaml-editor';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import React, { forwardRef, useImperativeHandle } from 'react'; import React, { forwardRef, useImperativeHandle } from 'react';
import { maasProviderType } from '../config';
import ProxyConfig from './proxy-config'; import ProxyConfig from './proxy-config';
const AdvanceConfig: React.FC<{ const AdvanceConfig: React.FC<{
action: PageActionType; action: PageActionType;
provider?: maasProviderType;
ref?: any; ref?: any;
}> = forwardRef(({ action, provider }, ref) => { }> = forwardRef(({ action }, ref) => {
const form = Form.useFormInstance(); const form = Form.useFormInstance();
const intl = useIntl(); const intl = useIntl();
const editorRef = React.useRef<any>(null); const editorRef = React.useRef<any>(null);
+15 -1
View File
@@ -1,5 +1,6 @@
import SealInput from '@/components/seal-form/seal-input'; import SealInput from '@/components/seal-form/seal-input';
import SealSelect from '@/components/seal-form/seal-select'; import SealSelect from '@/components/seal-form/seal-select';
import useAppUtils from '@/hooks/use-app-utils';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import ProviderLogo from '../components/provider-logo'; import ProviderLogo from '../components/provider-logo';
@@ -9,6 +10,7 @@ import { FormData } from '../config/types';
const Basic = () => { const Basic = () => {
const intl = useIntl(); const intl = useIntl();
const form = Form.useFormInstance<FormData>(); const form = Form.useFormInstance<FormData>();
const { getRuleMessage } = useAppUtils();
const optionRender = (option: any) => { const optionRender = (option: any) => {
return ( return (
@@ -29,9 +31,21 @@ const Basic = () => {
})} })}
/> />
</Form.Item> </Form.Item>
<Form.Item<FormData> name={['config', 'type']}> <Form.Item<FormData>
name={['config', 'type']}
rules={[
{
required: true,
message: getRuleMessage(
'select',
intl.formatMessage({ id: 'providers.table.providerName' })
)
}
]}
>
<SealSelect <SealSelect
showSearch showSearch
required
options={maasProviderOptions} options={maasProviderOptions}
optionRender={optionRender} optionRender={optionRender}
labelRender={optionRender} labelRender={optionRender}
+11 -17
View File
@@ -13,7 +13,6 @@ import {
useRef, useRef,
useState useState
} from 'react'; } from 'react';
import { maasProviderType } from '../config';
import FormContext from '../config/form-context'; import FormContext from '../config/form-context';
import { FormData, MaasProviderItem as ListItem } from '../config/types'; import { FormData, MaasProviderItem as ListItem } from '../config/types';
import AccessToken from './access-token'; import AccessToken from './access-token';
@@ -24,7 +23,6 @@ import SupportedModels from './supported-models';
interface ProviderFormProps { interface ProviderFormProps {
ref?: any; ref?: any;
action: PageActionType; action: PageActionType;
provider?: maasProviderType;
currentData?: ListItem; // Used when action is EDIT currentData?: ListItem; // Used when action is EDIT
onFinish: (values: FormData) => Promise<void>; onFinish: (values: FormData) => Promise<void>;
} }
@@ -37,7 +35,7 @@ const TABKeysMap = {
}; };
const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => { const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
const { action, provider, currentData, onFinish } = props; const { action, currentData, onFinish } = props;
const intl = useIntl(); const intl = useIntl();
const [form] = Form.useForm(); const [form] = Form.useForm();
const { getScrollElementScrollableHeight } = useWrapperContext(); const { getScrollElementScrollableHeight } = useWrapperContext();
@@ -83,10 +81,13 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
})); }));
useEffect(() => { useEffect(() => {
if (action === PageAction.EDIT && currentData) { if (
(action === PageAction.EDIT || action === PageAction.COPY) &&
currentData
) {
form.setFieldsValue({ form.setFieldsValue({
...currentData, ...currentData,
models: currentData.models || [] proxy_enabled: !!currentData.proxy_url
}); });
} }
}, [form, currentData, action]); }, [form, currentData, action]);
@@ -104,16 +105,14 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
}} }}
getScrollElementScrollableHeight={getScrollElementScrollableHeight} getScrollElementScrollableHeight={getScrollElementScrollableHeight}
> >
<FormContext.Provider value={{ providerType: provider, action }}> <FormContext.Provider value={{ action }}>
<Form <Form
form={form} form={form}
onFinish={onFinish} onFinish={onFinish}
initialValues={{ initialValues={{
proxy_config: { proxy_enabled: false,
enabled: false, proxy_url: '',
url: '', proxy_timeout: 30
timeout: 30
}
}} }}
> >
<Basic /> <Basic />
@@ -133,12 +132,7 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
key: TABKeysMap.ADVANCED, key: TABKeysMap.ADVANCED,
label: intl.formatMessage({ id: 'resources.form.advanced' }), label: intl.formatMessage({ id: 'resources.form.advanced' }),
forceRender: true, forceRender: true,
children: ( children: <AdvanceConfig action={action}></AdvanceConfig>
<AdvanceConfig
action={action}
provider={provider}
></AdvanceConfig>
)
} }
]} ]}
></CollapsePanel> ></CollapsePanel>
@@ -0,0 +1,134 @@
import SealSelect from '@/components/seal-form/seal-select';
import { categoryOptions } from '@/pages/llmodels/config';
import {
CheckCircleFilled,
CloseCircleFilled,
LoadingOutlined
} from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Form, Tooltip } from 'antd';
import React from 'react';
import styled from 'styled-components';
import { FormData, ProviderModel } from '../config/types';
import { useTestProviderModel } from '../hooks/use-query-provider-models';
const SelectWrapper = styled.div`
width: 100%;
display: grid;
grid-template-columns: 265px 160px max-content;
align-items: center;
gap: 8px;
.icon-wrapper {
display: flex;
align-items: center;
height: 100%;
}
`;
interface ModelItemProps {
onOpenChange: (open: boolean) => void;
onChange: (data: ProviderModel) => void;
providerModelList: ProviderModel[];
item: ProviderModel;
loading?: boolean;
}
const ModelItem: React.FC<ModelItemProps> = ({
onOpenChange,
onChange,
loading,
providerModelList,
item
}) => {
const intl = useIntl();
const form = Form.useFormInstance<FormData>();
const { runTestModel, loading: testLoading } = useTestProviderModel();
const handleTestModel = async () => {
const res = await runTestModel({
data: {
model_name: item.name,
api_token: form.getFieldValue('api_tokens')?.[0] || '',
config: {
type: form.getFieldValue(['config', 'type']) || ''
}
}
});
onChange({
...item,
accessible: res.accessible
});
};
const handleOnChange = (value: string) => {
onChange({
...item,
accessible: null,
name: value
});
};
const handleOnCategoryChange = (value: string) => {
onChange({
...item,
category: value
});
};
const renderSuffixIcon = () => {
if (testLoading) {
return <LoadingOutlined />;
}
if (item.accessible === true) {
return (
<CheckCircleFilled
style={{ color: 'var(--ant-color-success)', fontSize: 16 }}
/>
);
}
if (item.accessible === false) {
return (
<CloseCircleFilled
style={{ color: 'var(--ant-color-error)', fontSize: 16 }}
/>
);
}
return null;
};
return (
<SelectWrapper>
<SealSelect
loading={loading}
showSearch
onOpenChange={onOpenChange}
suffixIcon={renderSuffixIcon()}
alwaysFocus={true}
value={item.name}
onChange={handleOnChange}
options={providerModelList}
/>
<SealSelect
value={item.category}
onChange={handleOnCategoryChange}
options={categoryOptions}
placeholder={intl.formatMessage({
id: 'models.form.categories'
})}
></SealSelect>
<Tooltip
title={intl.formatMessage({ id: 'providers.form.model.test.tips' })}
>
<Button type="link" size="small" onClick={handleTestModel}>
{testLoading ? (
<LoadingOutlined />
) : (
intl.formatMessage({ id: 'providers.form.model.test' })
)}
</Button>
</Tooltip>
</SelectWrapper>
);
};
export default ModelItem;
+19 -11
View File
@@ -12,36 +12,44 @@ const AdvanceConfig = () => {
const intl = useIntl(); const intl = useIntl();
const { action } = useFormContext(); const { action } = useFormContext();
const proxyConfigRef = React.useRef<any>({}); const proxyConfigRef = React.useRef<any>({});
const proxyConfigEnabled = Form.useWatch(['proxy_config', 'enabled'], form); const proxyConfigEnabled = Form.useWatch('proxy_enabled', form);
const handleSpeculativeEnabledChange = (e: any) => { const handleProxyEnabledChange = (e: any) => {
if (e.target.checked) { if (e.target.checked) {
form.setFieldValue('speculative_config', { form.setFieldsValue({
enabled: true, proxy_enabled: true,
url: proxyConfigRef.current?.url || '', proxy_url: proxyConfigRef.current.proxy_url || '',
timeout: proxyConfigRef.current?.timeout || 30 proxy_timeout: proxyConfigRef.current.proxy_timeout || 30
}); });
} else { } else {
proxyConfigRef.current = form.getFieldValue('proxy_config'); proxyConfigRef.current = form.getFieldsValue([
'proxy_url',
'proxy_timeout'
]);
form.setFieldsValue({
proxy_enabled: false,
proxy_url: '',
proxy_timeout: 30
});
} }
}; };
return ( return (
<> <>
<Form.Item <Form.Item
name={['proxy_config', 'enabled']} name="proxy_enabled"
valuePropName="checked" valuePropName="checked"
style={{ marginBottom: 8 }} style={{ marginBottom: 8 }}
> >
<CheckboxField <CheckboxField
label={intl.formatMessage({ id: 'providers.form.proxy.enable' })} label={intl.formatMessage({ id: 'providers.form.proxy.enable' })}
onChange={handleSpeculativeEnabledChange} onChange={handleProxyEnabledChange}
></CheckboxField> ></CheckboxField>
</Form.Item> </Form.Item>
{proxyConfigEnabled && ( {proxyConfigEnabled && (
<> <>
<Form.Item <Form.Item
name={['proxy_config', 'url']} name="proxy_url"
rules={[ rules={[
{ {
required: true, required: true,
@@ -58,7 +66,7 @@ const AdvanceConfig = () => {
placeholder="http://proxy.example.com:8080" placeholder="http://proxy.example.com:8080"
></SealInput.Input> ></SealInput.Input>
</Form.Item> </Form.Item>
<Form.Item name={['proxy_config', 'timeout']}> <Form.Item name="proxy_timeout">
<SealInput.Number <SealInput.Number
label={intl.formatMessage({ id: 'providers.form.proxy.timeout' })} label={intl.formatMessage({ id: 'providers.form.proxy.timeout' })}
placeholder="30" placeholder="30"
@@ -1,88 +1,79 @@
import ListInput from '@/components/list-input'; import MetadataList from '@/components/metadata-list';
import SealSelect from '@/components/seal-form/seal-select';
import { CheckCircleFilled } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Form } from 'antd'; import { Form } from 'antd';
import styled from 'styled-components'; import { FormData, ProviderModel } from '../config/types';
import { FormData } from '../config/types'; import { useQueryProviderModels } from '../hooks/use-query-provider-models';
import ModelItem from './model-item';
const SelectWrapper = styled.div`
flex: 1;
display: flex;
align-items: center;
gap: 8px;
.icon-wrapper {
display: flex;
align-items: center;
height: 100%;
}
`;
const SupportedModels = () => { const SupportedModels = () => {
const intl = useIntl(); const intl = useIntl();
const { providerModelList, loading, fetchProviderModels } =
useQueryProviderModels();
const form = Form.useFormInstance<FormData>(); const form = Form.useFormInstance<FormData>();
const modelList = Form.useWatch('models', form) || [];
const handleTestModel = (data: any) => { const handleOpenChange = async (open: boolean) => {
data.loading = true; try {
setTimeout(() => { await form.validateFields(['api_tokens', ['config', 'type']]);
data.loading = false;
}, 1000);
};
const renderModelItem = ( if (open && providerModelList.length === 0) {
data: any, fetchProviderModels({
{ data: {
onChange, api_token: form.getFieldValue('api_tokens')?.[0] || '',
onBlur config: {
}: { onChange: (value: string) => void; onBlur?: (e: any) => void } type: form.getFieldValue(['config', 'type']) || ''
) => { }
return (
<SelectWrapper>
<SealSelect
suffixIcon={
<CheckCircleFilled
style={{ color: 'var(--ant-color-success)', fontSize: 16 }}
/>
} }
alwaysFocus={true} });
value={data.value} }
onChange={onChange} } catch (error) {}
onBlur={onBlur}
options={[
{ label: 'Model A', value: 'model_a' },
{ label: 'Model B', value: 'model_b' },
{ label: 'Model C', value: 'model_c' }
]}
/>
<Button
type="link"
size="small"
loading={data.loading}
onClick={() => handleTestModel(data)}
>
{intl.formatMessage({ id: 'providers.form.model.test' })}
</Button>
</SelectWrapper>
);
}; };
const handleOnChange = (values: string[]) => {}; const updateModelList = (models: ProviderModel[]) => {
form.setFieldsValue({
models: models
});
};
const onAdd = () => {
const newList = [...modelList];
newList.push({ name: '', category: '', accessible: null });
updateModelList(newList);
};
const onDelete = (index: number) => {
const newList = [...modelList];
newList.splice(index, 1);
updateModelList(newList);
};
const onChange = (data: ProviderModel, index: number) => {
const newList = [...modelList];
newList[index] = { ...data };
updateModelList(newList);
};
return ( return (
<> <>
<Form.Item name="models"> <Form.Item name="models" data-field="supportedModels">
<div data-field="supportedModels"></div> <MetadataList
<ListInput dataList={modelList}
styles={{ label={intl.formatMessage({ id: 'providers.table.models' })}
wrapper: {
paddingTop: 14
}
}}
btnText={intl.formatMessage({ id: 'providers.form.models.add' })} btnText={intl.formatMessage({ id: 'providers.form.models.add' })}
dataList={[]} onAdd={onAdd}
renderItem={renderModelItem} onDelete={onDelete}
onChange={handleOnChange} >
></ListInput> {(item, index) => (
<ModelItem
key={index}
onOpenChange={handleOpenChange}
onChange={(data) => onChange(data, index)}
loading={loading}
providerModelList={providerModelList}
item={item}
></ModelItem>
)}
</MetadataList>
</Form.Item> </Form.Item>
</> </>
); );
@@ -1,7 +1,6 @@
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import { useState } from 'react'; import { useState } from 'react';
import { maasProviderType } from '../config';
import { MaasProviderItem as ListItem } from '../config/types'; import { MaasProviderItem as ListItem } from '../config/types';
const useCreateProvider = (options: { refresh: () => void }) => { const useCreateProvider = (options: { refresh: () => void }) => {
@@ -10,13 +9,11 @@ const useCreateProvider = (options: { refresh: () => void }) => {
action: PageActionType; action: PageActionType;
currentData?: ListItem; currentData?: ListItem;
title: string; title: string;
provider: maasProviderType | null;
}>({ }>({
open: false, open: false,
action: PageAction.CREATE, action: PageAction.CREATE,
currentData: undefined, currentData: undefined,
title: '', title: ''
provider: null
}); });
const openModal = (action: PageActionType, title: string, row?: ListItem) => { const openModal = (action: PageActionType, title: string, row?: ListItem) => {
@@ -25,8 +22,7 @@ const useCreateProvider = (options: { refresh: () => void }) => {
open: true, open: true,
title: title, title: title,
action, action,
currentData: row, currentData: row
provider: row ? row.provider : null
}); });
}; };
@@ -35,8 +31,7 @@ const useCreateProvider = (options: { refresh: () => void }) => {
open: false, open: false,
action: PageAction.CREATE, action: PageAction.CREATE,
currentData: undefined, currentData: undefined,
title: '', title: ''
provider: null
}); });
}; };
@@ -10,7 +10,7 @@ import { useMemo } from 'react';
import ProviderLogo from '../components/provider-logo'; import ProviderLogo from '../components/provider-logo';
import ProviderModels from '../components/provider-models'; import ProviderModels from '../components/provider-models';
import { maasProviderLabelMap, rowActionList } from '../config'; import { maasProviderLabelMap, rowActionList } from '../config';
import { MaasProviderItem } from '../config/types'; import { MaasProviderItem, ProviderModel } from '../config/types';
const useProviderColumns = ( const useProviderColumns = (
handleSelect: (val: string, record: MaasProviderItem) => void, handleSelect: (val: string, record: MaasProviderItem) => void,
@@ -41,7 +41,7 @@ const useProviderColumns = (
}, },
{ {
title: intl.formatMessage({ id: 'providers.table.providerName' }), title: intl.formatMessage({ id: 'providers.table.providerName' }),
dataIndex: 'provider', dataIndex: ['config', 'type'],
sorter: tableSorter(2), sorter: tableSorter(2),
span: 4, span: 4,
minWidth: 160, minWidth: 160,
@@ -61,8 +61,9 @@ const useProviderColumns = (
dataIndex: 'models', dataIndex: 'models',
span: 3, span: 3,
minWidth: 200, minWidth: 200,
sorter: tableSorter(3), render: (value: ProviderModel[]) => (
render: (value: number) => <ProviderModels></ProviderModels> <ProviderModels dataList={value || []}></ProviderModels>
)
}, },
{ {
title: intl.formatMessage({ id: 'common.table.createTime' }), title: intl.formatMessage({ id: 'common.table.createTime' }),
@@ -0,0 +1,102 @@
import { createAxiosToken } from '@/hooks/use-chunk-request';
import { useRequest } from 'ahooks';
import { message } from 'antd';
import { CancelTokenSource } from 'axios';
import { useEffect, useRef, useState } from 'react';
import { queryProviderModels, testProviderModel } from '../apis';
/**
*
* @returns loading, fetch, dataList
*/
export const useQueryProviderModels = () => {
const axiosTokenRef = useRef<CancelTokenSource | null>(null);
const [providerModelList, setProviderModelList] = useState<any[]>([]);
const {
run: fetchProviderModels,
loading,
cancel
} = useRequest(
async (params: {
data: { api_token: string; config: { type: string } };
}) => {
axiosTokenRef.current?.cancel();
axiosTokenRef.current = createAxiosToken();
return await queryProviderModels(params, {
token: axiosTokenRef.current.token
});
},
{
manual: true,
onSuccess: (response) => {
setProviderModelList(
response.data?.map((item: any) => ({
label: item.id,
value: item.id
})) || []
);
},
onError: () => {
setProviderModelList([]);
}
}
);
useEffect(() => {
return () => {
cancel();
axiosTokenRef.current?.cancel();
};
}, []);
return {
loading,
providerModelList,
fetchProviderModels
};
};
export const useTestProviderModel = () => {
const axiosTokenRef = useRef<CancelTokenSource | null>(null);
const {
runAsync: runTestModel,
loading,
cancel
} = useRequest(
async (params: {
data: { api_token: string; config: { type: string }; model_name: string };
}) => {
axiosTokenRef.current?.cancel();
axiosTokenRef.current = createAxiosToken();
const response = await testProviderModel(params, {
token: axiosTokenRef.current.token
});
return response;
},
{
manual: true,
onSuccess: (response) => {
if (!response?.accessible) {
message.error(response?.error_message || 'Test model failed');
}
},
onError: (error) => {
message.error(error?.message || 'Test model failed');
}
}
);
useEffect(() => {
return () => {
cancel();
axiosTokenRef.current?.cancel();
};
}, []);
return {
loading,
runTestModel
};
};
+13 -16
View File
@@ -7,7 +7,6 @@ import { PageAction } from '@/config';
import { TABLE_SORT_DIRECTIONS } from '@/config/settings'; import { TABLE_SORT_DIRECTIONS } from '@/config/settings';
import useExpandedRowKeys from '@/hooks/use-expanded-row-keys'; import useExpandedRowKeys from '@/hooks/use-expanded-row-keys';
import useTableFetch from '@/hooks/use-table-fetch'; import useTableFetch from '@/hooks/use-table-fetch';
import useWatchList from '@/hooks/use-watch-list';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { ConfigProvider, message, Table } from 'antd'; import { ConfigProvider, message, Table } from 'antd';
@@ -16,16 +15,15 @@ import _ from 'lodash';
import NoResult from '../_components/no-result'; import NoResult from '../_components/no-result';
import PageBox from '../_components/page-box'; import PageBox from '../_components/page-box';
import { import {
createProvider,
deleteProvider, deleteProvider,
MAAS_PROVIDERS_API, MAAS_PROVIDERS_API,
PROVIDER_MODELS_API,
queryMaasProviders, queryMaasProviders,
updateProvider updateProvider
} from './apis'; } from './apis';
import AddMaasProvider from './components/add-provider-modal'; import AddMaasProvider from './components/add-provider-modal';
import ProviderModels from './components/provider-models'; import ProviderModels from './components/provider-models';
import { maasProviderOptions } from './config'; import { maasProviderOptions } from './config';
import { mockDataList } from './config/mock';
import { import {
FormData, FormData,
MaasProviderItem as ListItem, MaasProviderItem as ListItem,
@@ -50,12 +48,10 @@ const MaasProvider: React.FC = () => {
} = useTableFetch<ListItem>({ } = useTableFetch<ListItem>({
fetchAPI: queryMaasProviders, fetchAPI: queryMaasProviders,
deleteAPI: deleteProvider, deleteAPI: deleteProvider,
watch: true, watch: false,
API: MAAS_PROVIDERS_API, API: MAAS_PROVIDERS_API,
contentForDelete: 'menu.models.providers' contentForDelete: 'menu.models.providers'
}); });
const { watchDataList: allProviderModels } =
useWatchList(PROVIDER_MODELS_API);
const [expandAtom] = useAtom(expandKeysAtom); const [expandAtom] = useAtom(expandKeysAtom);
const { handleExpandChange, handleExpandAll, expandedRowKeys } = const { handleExpandChange, handleExpandAll, expandedRowKeys } =
useExpandedRowKeys(expandAtom); useExpandedRowKeys(expandAtom);
@@ -83,6 +79,14 @@ const MaasProvider: React.FC = () => {
id: openProviderModalStatus.currentData!.id id: openProviderModalStatus.currentData!.id
}); });
} }
if (
openProviderModalStatus.action === PageAction.CREATE ||
openProviderModalStatus.action === PageAction.COPY
) {
await createProvider({
data: params
});
}
fetchData(); fetchData();
closeProviderModal(); closeProviderModal();
message.success(intl.formatMessage({ id: 'common.message.success' })); message.success(intl.formatMessage({ id: 'common.message.success' }));
@@ -112,7 +116,7 @@ const MaasProvider: React.FC = () => {
handleDelete({ ...row, name: row.name }); handleDelete({ ...row, name: row.name });
} }
if (val === 'copy') { if (val === 'copy') {
openProviderModal(PageAction.EDIT, 'Copy Provider', row); openProviderModal(PageAction.COPY, 'Copy Provider', row);
} }
}); });
@@ -149,13 +153,7 @@ const MaasProvider: React.FC = () => {
list: any, list: any,
options: { parent?: any; [key: string]: any } options: { parent?: any; [key: string]: any }
) => { ) => {
return ( return <ProviderModels dataList={list} />;
<ProviderModels
dataList={list}
provider={options.parent?.provider}
providerId={options.parent?.id}
/>
);
}; };
const renderEmpty = (type?: string) => { const renderEmpty = (type?: string) => {
@@ -215,7 +213,7 @@ const MaasProvider: React.FC = () => {
renderChildren={renderChildren} renderChildren={renderChildren}
onTableSort={handleOnSortChange} onTableSort={handleOnSortChange}
showSorterTooltip={false} showSorterTooltip={false}
dataSource={mockDataList} dataSource={dataSource.dataList}
loading={dataSource.loading} loading={dataSource.loading}
loadend={dataSource.loadend} loadend={dataSource.loadend}
rowSelection={rowSelection} rowSelection={rowSelection}
@@ -235,7 +233,6 @@ const MaasProvider: React.FC = () => {
</ConfigProvider> </ConfigProvider>
</PageBox> </PageBox>
<AddMaasProvider <AddMaasProvider
provider={openProviderModalStatus.provider}
open={openProviderModalStatus.open} open={openProviderModalStatus.open}
action={openProviderModalStatus.action} action={openProviderModalStatus.action}
title={openProviderModalStatus.title} title={openProviderModalStatus.title}
+3 -3
View File
@@ -1,9 +1,9 @@
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import { AccessItem, FormData } from '../config/types'; import { AccessItem, FormData } from '../config/types';
export const ACCESS_API = '/accesses'; export const ACCESS_API = '/model-accesses';
export const ACCESS_POINTS_API = '/models'; export const ACCESS_POINTS_API = '/model-access-endpoints';
export async function queryModelAccesses( export async function queryModelAccesses(
params: Global.SearchParams, params: Global.SearchParams,
@@ -38,7 +38,7 @@ export async function deleteAccess(id: number) {
export async function queryAccessPoints(params: { id: string }, options?: any) { export async function queryAccessPoints(params: { id: string }, options?: any) {
return request<Global.BaseOption<string>[]>( return request<Global.BaseOption<string>[]>(
`${ACCESS_API}/${params.id}/points`, `${ACCESS_API}/${params.id}/access`,
{ {
method: 'GET', method: 'GET',
cancelToken: options?.token cancelToken: options?.token
-41
View File
@@ -2,47 +2,6 @@ import icons from '@/components/icon-font/icons';
import { StatusMaps } from '@/config'; import { StatusMaps } from '@/config';
import { StatusType } from '@/config/types'; import { StatusType } from '@/config/types';
export type maasProviderType =
| 'doubao'
| 'qwen'
| 'openai'
| 'deepseek'
| 'anthropic';
export const maasProviderValueMap = {
Doubao: 'doubao',
Qwen: 'qwen',
OpenAI: 'openai',
Deepseek: 'deepseek',
Anthropic: 'anthropic'
};
export const maasProviderLabelMap = {
[maasProviderValueMap.Doubao]: 'Doubao',
[maasProviderValueMap.Qwen]: 'Qwen',
[maasProviderValueMap.OpenAI]: 'OpenAI',
[maasProviderValueMap.Deepseek]: 'Deepseek',
[maasProviderValueMap.Anthropic]: 'Anthropic'
};
export const providerIconsMap = {
[maasProviderValueMap.Doubao]: 'icon-doubao',
[maasProviderValueMap.Qwen]: 'icon-qwen',
[maasProviderValueMap.OpenAI]: 'icon-openai',
[maasProviderValueMap.Deepseek]: 'icon-deepseek',
[maasProviderValueMap.Anthropic]: 'icon-anthropic'
};
export const maasProviderOptions = Object.entries(maasProviderValueMap).map(
([key, value]) => ({
label: maasProviderLabelMap[value],
value: value,
key: value,
locale: false,
icon: providerIconsMap[value]
})
);
export const ProviderStatusValueMap: Record<string, string> = { export const ProviderStatusValueMap: Record<string, string> = {
Ready: 'Ready', Ready: 'Ready',
InActive: 'Inactive' InActive: 'Inactive'
-7
View File
@@ -27,7 +27,6 @@ import {
} from './apis'; } from './apis';
import AccessPoints from './components/access-points'; import AccessPoints from './components/access-points';
import AddAccessModal from './components/add-access-modal'; import AddAccessModal from './components/add-access-modal';
import { maasProviderOptions } from './config';
import { mockDataList } from './config/mock'; import { mockDataList } from './config/mock';
import { FormData, AccessItem as ListItem } from './config/types'; import { FormData, AccessItem as ListItem } from './config/types';
import useAccessColumns from './hooks/use-access-columns'; import useAccessColumns from './hooks/use-access-columns';
@@ -186,12 +185,6 @@ const Accesses: React.FC = () => {
handleSearch={handleSearch} handleSearch={handleSearch}
handleDeleteByBatch={handleDeleteBatch} handleDeleteByBatch={handleDeleteBatch}
handleClickPrimary={handleClickDropdown} handleClickPrimary={handleClickDropdown}
actionItems={maasProviderOptions.map((option) => ({
...option,
icon: (
<IconFont type={option.icon as string} style={{ fontSize: 14 }} />
)
}))}
></FilterBar> ></FilterBar>
<TableContext.Provider <TableContext.Provider
value={{ value={{