Compare commits

...
10 Commits
17 changed files with 138 additions and 76 deletions
+5 -6
View File
@@ -80,13 +80,12 @@ const LabelSelector: React.FC<LabelSelectorProps> = ({
if (!clipboardText || clipboardText.indexOf('=') === -1) return; if (!clipboardText || clipboardText.indexOf('=') === -1) return;
e.preventDefault(); e.preventDefault();
const lines = clipboardText const lines = _.split(clipboardText, /\r?\n/)
.split(/\r?\n/) .map((line: string) => line.trim())
.map((line) => line.trim()) .filter((line: string) => line && line.includes('='));
.filter((line) => line && line.includes('='));
const parsedData = lines.map((line) => { const parsedData = lines.map((line: string) => {
const [key, value] = line.split('=').map((part) => part.trim()); const [key, value] = line.split(/=(.+)/).map((s) => s.trim());
return { key, value }; return { key, value };
}); });
+4 -5
View File
@@ -92,7 +92,7 @@ export function useQueryData<Detail, Params = any>(option: {
key: string; key: string;
delay?: number; delay?: number;
fetchDetail: (params: Params, options?: any) => Promise<Detail>; fetchDetail: (params: Params, options?: any) => Promise<Detail>;
getData?: (response: Detail) => any; getData?: (response: Detail, params?: any) => any;
errorMsg?: string; errorMsg?: string;
}): { }): {
loading: boolean; loading: boolean;
@@ -123,7 +123,7 @@ export function useQueryData<Detail, Params = any>(option: {
}); });
} }
setDetailData(getData ? getData(res) : res); setDetailData(getData ? getData(res, params) : res);
return res; return res;
}, },
@@ -132,7 +132,7 @@ export function useQueryData<Detail, Params = any>(option: {
onSuccess: () => {}, onSuccess: () => {},
onError: (error) => { onError: (error) => {
message.error( message.error(
error?.message || errorMsg || `Failed to fetch ${key} list` error?.message || errorMsg || `Failed to fetch ${key} data`
); );
setDetailData({} as Detail); setDetailData({} as Detail);
} }
@@ -146,8 +146,7 @@ export function useQueryData<Detail, Params = any>(option: {
useEffect(() => { useEffect(() => {
return () => { return () => {
cancel(); cancelRequest();
axiosTokenRef.current?.cancel();
}; };
}, []); }, []);
@@ -1,7 +1,8 @@
import AutoTooltip from '@/components/auto-tooltip'; import AutoTooltip from '@/components/auto-tooltip';
import FullMarkdown from '@/components/markdown-viewer/full-markdown';
import { BulbOutlined } from '@ant-design/icons'; import { BulbOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Tag, Typography } from 'antd'; import { Button, Tag } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
@@ -146,9 +147,7 @@ const BackendDetail: React.FC<{
</span> </span>
</Subtitle> </Subtitle>
<Content> <Content>
<Typography.Paragraph style={{ whiteSpace: 'pre-line' }}> <FullMarkdown content={currentData?.description}></FullMarkdown>
{currentData?.description}
</Typography.Paragraph>
</Content> </Content>
</Section> </Section>
)} )}
+1 -5
View File
@@ -145,11 +145,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
is_built_in: is_built_in:
data.is_built_in && data.is_built_in &&
data.backend_source === BackendSourceValueMap.BUILTIN, data.backend_source === BackendSourceValueMap.BUILTIN,
..._.pick(values.built_in_version_configs?.[key], [ ..._.pick(values.built_in_version_configs?.[key], versionFields)
'image_name',
'run_command',
'entrypoint'
])
})); }));
return { return {
+5 -2
View File
@@ -192,7 +192,8 @@ export const customBackendFields = [
'health_check_path', 'health_check_path',
'default_run_command', 'default_run_command',
'version_configs', 'version_configs',
'default_backend_param' 'default_backend_param',
'default_env'
]; ];
/** /**
@@ -201,7 +202,8 @@ export const customBackendFields = [
export const builtInBackendFields = [ export const builtInBackendFields = [
'description', 'description',
'version_configs', 'version_configs',
'default_backend_param' 'default_backend_param',
'default_env'
]; ];
export const frameworks = [ export const frameworks = [
@@ -278,6 +280,7 @@ export const yamlTemplate = `# ----------------------------------------
# - custom_framework: # - custom_framework:
# - required # - required
# - choose from: ${Object.values(GPUDriverMap).join(', ')}, cpu # - choose from: ${Object.values(GPUDriverMap).join(', ')}, cpu
# - env: optional, map of env key and value
backend_name: vllm-custom backend_name: vllm-custom
description: this is my custom vllm backend description: this is my custom vllm backend
+2 -1
View File
@@ -201,6 +201,7 @@ const VersionsForm: React.FC<AddModalProps> = ({
}; };
const isBuiltin = backendSource === BackendSourceValueMap.BUILTIN; const isBuiltin = backendSource === BackendSourceValueMap.BUILTIN;
const isCommunity = backendSource === BackendSourceValueMap.COMMUNITY;
return ( return (
<> <>
@@ -305,7 +306,7 @@ const VersionsForm: React.FC<AddModalProps> = ({
></Form.Item> ></Form.Item>
)} )}
</span> </span>
{(fields.length > 1 || isBuiltin) && ( {(fields.length > 1 || isBuiltin || isCommunity) && (
<Button <Button
size="small" size="small"
shape="circle" shape="circle"
@@ -1,5 +1,6 @@
import IconFont from '@/components/icon-font'; import IconFont from '@/components/icon-font';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import useBodyScroll from '@/hooks/use-body-scroll'; import useBodyScroll from '@/hooks/use-body-scroll';
import { ListItem } from '../config/types'; import { ListItem } from '../config/types';
import useCommunityBackend from './use-community-backend'; import useCommunityBackend from './use-community-backend';
@@ -30,7 +31,7 @@ const useCreateBackend = () => {
]; ];
const handleEditBackend = ( const handleEditBackend = (
action: PageAction, action: PageActionType,
title: string, title: string,
row: ListItem row: ListItem
) => { ) => {
+8 -32
View File
@@ -1,46 +1,22 @@
import { useMemoizedFn } from 'ahooks';
import { Spin } from 'antd'; import { Spin } from 'antd';
import { omit } from 'lodash'; import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import PageBox from '../_components/page-box'; import PageBox from '../_components/page-box';
import { queryDashboardData } from './apis';
import DashboardInner from './components/dahboard-inner'; import DashboardInner from './components/dahboard-inner';
import DashboardContext from './config/dashboard-context'; import DashboardContext from './config/dashboard-context';
import { DashboardProps } from './config/types'; import useQueryDashboard from './services/use-query-dashboard';
const Dashboard: React.FC = () => { const Dashboard: React.FC = () => {
const [data, setData] = useState<DashboardProps>({} as DashboardProps); const { fetchData, loading, data, cancelRequest } = useQueryDashboard();
const [loading, setLoading] = useState(false);
const fetchDashboardData = useMemoizedFn(
async (params?: { cluster_id?: number }) => {
try {
setLoading(true);
const res = await queryDashboardData(params);
setData((prev) => {
return params?.cluster_id
? {
...omit(prev, ['system_load']),
system_load: res.system_load
}
: res;
});
} catch (error) {
setData({} as DashboardProps);
} finally {
setLoading(false);
}
}
);
useEffect(() => { useEffect(() => {
fetchDashboardData(); fetchData({});
return () => {
cancelRequest();
};
}, []); }, []);
return ( return (
<DashboardContext.Provider <DashboardContext.Provider value={{ ...data, fetchData: fetchData }}>
value={{ ...data, fetchData: fetchDashboardData }}
>
<PageBox> <PageBox>
<Spin spinning={loading} style={{ minHeight: 300 }}> <Spin spinning={loading} style={{ minHeight: 300 }}>
<DashboardInner /> <DashboardInner />
@@ -0,0 +1,25 @@
import { useQueryData } from '@/hooks/use-query-data-list';
import { omit } from 'lodash';
import { queryDashboardData } from '../apis';
export default function useQueryDashboard() {
const { detailData, loading, fetchData, cancelRequest } = useQueryData({
key: 'dashboard',
fetchDetail: queryDashboardData,
getData(response, params) {
return params?.cluster_id
? {
...omit(detailData, ['system_load']),
system_load: response.system_load
}
: response;
}
});
return {
loading,
data: detailData,
cancelRequest,
fetchData
};
}
@@ -7,6 +7,7 @@ interface FormContextProps {
action: PageActionType; action: PageActionType;
currentData?: any; currentData?: any;
id?: number; id?: number;
getCustomConfig?: () => Record<string, any>;
} }
const FormContext = createContext<FormContextProps>({} as FormContextProps); const FormContext = createContext<FormContextProps>({} as FormContextProps);
+1
View File
@@ -12,6 +12,7 @@ export interface FormData {
api_key: string; api_key: string;
proxy_url: string; proxy_url: string;
proxy_timeout: number; proxy_timeout: number;
proxy_enabled?: boolean;
config: { config: {
type: maasProviderType; type: maasProviderType;
openaiCustomUrl?: string; openaiCustomUrl?: string;
+12 -1
View File
@@ -97,12 +97,18 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
); );
}; };
const getCustomConfig = () => {
const customConfig = yaml2Json(advanceRef.current?.getYamlValue() || '');
return customConfig;
};
const handleOnFinish = (values: FormData) => { const handleOnFinish = (values: FormData) => {
const data = { const data = {
..._.omit(values, ['api_key']), ..._.omit(values, ['api_key']),
api_tokens: formatAPIKeys(values), api_tokens: formatAPIKeys(values),
config: { config: {
type: values.config.type, type: values.config.type,
openaiCustomUrl: values.config.openaiCustomUrl || undefined,
...yaml2Json(advanceRef.current?.getYamlValue() || '') ...yaml2Json(advanceRef.current?.getYamlValue() || '')
}, },
models: _.uniqBy(values.models, 'name'), models: _.uniqBy(values.models, 'name'),
@@ -170,7 +176,12 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
getScrollElementScrollableHeight={getScrollElementScrollableHeight} getScrollElementScrollableHeight={getScrollElementScrollableHeight}
> >
<FormContext.Provider <FormContext.Provider
value={{ action, id: currentData?.id, currentData }} value={{
action,
id: currentData?.id,
currentData,
getCustomConfig: getCustomConfig
}}
> >
<Form <Form
form={form} form={form}
+12 -3
View File
@@ -54,7 +54,7 @@ const ModelItem: React.FC<ModelItemProps> = ({
const intl = useIntl(); const intl = useIntl();
const form = Form.useFormInstance<FormData>(); const form = Form.useFormInstance<FormData>();
const { runTestModel, loading: testLoading } = useTestProviderModel(); const { runTestModel, loading: testLoading } = useTestProviderModel();
const { id, action, currentData } = useFormContext(); const { id, action, currentData, getCustomConfig } = useFormContext();
const [openTip, setOpenTip] = React.useState(false); const [openTip, setOpenTip] = React.useState(false);
const generateCurrentAPIKey = (currentAPIKey: string) => { const generateCurrentAPIKey = (currentAPIKey: string) => {
@@ -75,6 +75,8 @@ const ModelItem: React.FC<ModelItemProps> = ({
}; };
const handleTestModel = async () => { const handleTestModel = async () => {
const proxyConfigEnabled = form.getFieldValue('proxy_enabled');
const customConfig = getCustomConfig?.();
const res = await runTestModel({ const res = await runTestModel({
id: generateID(), id: generateID(),
data: { data: {
@@ -82,9 +84,16 @@ const ModelItem: React.FC<ModelItemProps> = ({
api_token: generateCurrentAPIKey( api_token: generateCurrentAPIKey(
form.getFieldValue('api_key') form.getFieldValue('api_key')
) as string, ) as string,
proxy_url: form.getFieldValue('proxy_url') || undefined, proxy_url: proxyConfigEnabled
? form.getFieldValue('proxy_url') || null
: null,
config: { config: {
type: form.getFieldValue(['config', 'type']) || '' type: form.getFieldValue(['config', 'type']) || '',
...customConfig,
openaiCustomUrl:
customConfig?.openaiCustomUrl ||
form.getFieldValue(['config', 'openaiCustomUrl']) ||
null
} }
} }
}); });
@@ -1,8 +1,8 @@
import MetadataList from '@/components/metadata-list'; import MetadataList from '@/components/metadata-list';
import { PageAction } from '@/config'; import { PageAction } from '@/config';
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 _ from 'lodash';
import { useRef } from 'react'; import { useRef } from 'react';
import { useFormContext } from '../config/form-context'; import { useFormContext } from '../config/form-context';
import { FormData, ProviderModel } from '../config/types'; import { FormData, ProviderModel } from '../config/types';
@@ -15,9 +15,12 @@ const SupportedModels = () => {
useQueryProviderModels(); useQueryProviderModels();
const form = Form.useFormInstance<FormData>(); const form = Form.useFormInstance<FormData>();
const modelList = Form.useWatch('models', form) || []; const modelList = Form.useWatch('models', form) || [];
const prevAPIKeyRef = useRef<string>(''); const prevConfigRef = useRef<{
const { getRuleMessage } = useAppUtils(); type: string;
const { id, action, currentData } = useFormContext(); api_key: string;
openaiCustomUrl: string;
}>({ type: '', api_key: '', openaiCustomUrl: '' });
const { id, action, currentData, getCustomConfig } = useFormContext();
const generateCurrentAPIKey = (currentAPIKey: string) => { const generateCurrentAPIKey = (currentAPIKey: string) => {
if ( if (
@@ -36,28 +39,63 @@ const SupportedModels = () => {
return 0; return 0;
}; };
const checkConfigChange = (current: {
type: string;
api_key: string;
openaiCustomUrl: string;
}) => {
return (
!_.isEqual(current, prevConfigRef.current) &&
current.api_key &&
current.type
);
};
const handleOpenChange = async (open: boolean) => { const handleOpenChange = async (open: boolean) => {
try { try {
await form.validateFields(['api_key', ['config', 'type']]); await form.validateFields(['api_key', ['config', 'type']]);
const proxyConfigEnabled = form.getFieldValue('proxy_enabled');
const currentAPIKey = form.getFieldValue('api_key') || ''; const currentAPIKey = form.getFieldValue('api_key') || '';
const configType = form.getFieldValue(['config', 'type']);
const openaiCustomUrl = form.getFieldValue(['config', 'openaiCustomUrl']);
const customConfig = getCustomConfig?.();
const currentConfig = {
type: configType,
api_key: currentAPIKey,
openaiCustomUrl: customConfig?.openaiCustomUrl || openaiCustomUrl || ''
};
// Avoid repeated requests with the same API key // Avoid repeated requests with the same API key
if (open && prevAPIKeyRef.current !== currentAPIKey && currentAPIKey) { if (open && checkConfigChange(currentConfig)) {
prevAPIKeyRef.current = currentAPIKey; prevConfigRef.current = {
...currentConfig
};
fetchProviderModels({ fetchProviderModels({
id: generateID(), id: generateID(),
data: { data: {
api_token: generateCurrentAPIKey(currentAPIKey) as string, api_token: generateCurrentAPIKey(currentAPIKey) as string,
proxy_url: form.getFieldValue('proxy_url') || undefined, proxy_url: proxyConfigEnabled
? form.getFieldValue('proxy_url') || null
: null,
config: { config: {
type: form.getFieldValue(['config', 'type']) || '' type: form.getFieldValue(['config', 'type']) || '',
...customConfig,
openaiCustomUrl:
customConfig?.openaiCustomUrl || openaiCustomUrl || null
} }
} }
}); });
} }
} catch (error) { } catch (error) {
prevAPIKeyRef.current = ''; prevConfigRef.current = {
type: '',
api_key: '',
openaiCustomUrl: ''
};
// If validation fails, reset the provider model list to avoid confusion
} }
}; };
@@ -26,7 +26,11 @@ export const useQueryProviderModels = () => {
} = useRequest( } = useRequest(
async (params: { async (params: {
id: number; id: number;
data: { api_token: string; config: { type: string }; proxy_url: string }; data: {
api_token: string;
config: { type: string; [key: string]: any };
proxy_url: string;
};
}) => { }) => {
axiosTokenRef.current?.cancel(); axiosTokenRef.current?.cancel();
axiosTokenRef.current = createAxiosToken(); axiosTokenRef.current = createAxiosToken();
@@ -83,7 +87,7 @@ export const useTestProviderModel = () => {
id: number; id: number;
data: { data: {
api_token: string; api_token: string;
config: { type: string }; config: { type: string; [key: string]: any };
model_name: string; model_name: string;
proxy_url: string; proxy_url: string;
}; };
+1 -1
View File
@@ -318,7 +318,7 @@ const ModelRoutes: React.FC = () => {
loading={dataSource.loading} loading={dataSource.loading}
loadend={dataSource.loadend} loadend={dataSource.loadend}
dataSource={dataSource.dataList} dataSource={dataSource.dataList}
image={<IconFont type="icon-extension-outline" />} image={<IconFont type="icon-captive_portal" />}
filters={_.omit(queryParams, ['sort_by'])} filters={_.omit(queryParams, ['sort_by'])}
noFoundText={intl.formatMessage({ noFoundText={intl.formatMessage({
id: 'noresult.routes.nofound' id: 'noresult.routes.nofound'
@@ -53,15 +53,14 @@ export default function useChatCompletion(
} }
const deltaReasoningContent = const deltaReasoningContent =
_.get(chunk, 'choices.0.delta.reasoning_content', '') === null _.get(chunk, 'choices.0.delta.reasoning_content', '') ||
? '' _.get(chunk, 'choices.0.delta.reasoning', '') ||
: _.get(chunk, 'choices.0.delta.reasoning_content', ''); '';
const deltaContent = const deltaContent =
_.get(chunk, 'choices.0.delta.content', '') === null _.get(chunk, 'choices.0.delta.content', '') === null
? '' ? ''
: _.get(chunk, 'choices.0.delta.content', ''); : _.get(chunk, 'choices.0.delta.content', '');
console.log('deltaContent:', deltaContent);
reasonContentRef.current = reasonContentRef.current + deltaReasoningContent; reasonContentRef.current = reasonContentRef.current + deltaReasoningContent;
contentRef.current = contentRef.current + deltaContent; contentRef.current = contentRef.current + deltaContent;