Compare commits

...
32 Commits
Author SHA1 Message Date
jialin c5203c01d0 chore: remove experimental from metax 2026-04-15 16:45:13 +08:00
jialin e7a9d2af00 chore: sharegpt description 2026-03-24 17:31:31 +08:00
jialin 59740d1601 fix: a non-running instance is selected in creating benchmark 2026-03-24 17:10:18 +08:00
jialin d874e502e2 fix: add ShareGPT profile 2026-03-24 16:20:03 +08:00
jialin 722b385bcb feat: add --openai-support 2026-03-23 15:59:11 +08:00
jialin aa7247baaf fix: copy failed in non-localhost and non-https 2026-03-23 12:31:45 +08:00
jialin 73f3cfceb1 fix(style): error message overlap input box 2026-03-19 14:02:16 +08:00
jialin f2fe080f7b fix: bedrock required fields 2026-03-18 18:31:29 +08:00
jialin eee73be77e fix: typos: position 2026-03-18 15:19:06 +08:00
jialin 6e4dd30104 fix: show required fields for provider 2026-03-18 15:01:04 +08:00
jialin 19b88f3375 fix: open the advanced when configured 2026-03-17 17:29:38 +08:00
jialin 94b3206111 fix: reset filter in export modal 2026-03-16 14:44:18 +08:00
jialin ea4ea56e59 fix: show backend warning hint after submitting in editing mode 2026-03-16 14:06:41 +08:00
jialin 042f8fed47 chore: update model cols 2026-03-16 11:11:46 +08:00
jialin f7c3b28cc8 style: same key tooltip 2026-03-16 10:31:57 +08:00
jialin 1462768aa9 fix: title for editing apikey 2026-03-12 19:34:14 +08:00
jialin 07fa3c8824 feat: add profile filter in benchmark 2026-03-12 18:51:10 +08:00
jialin 61f83f31d7 chore: add nvidia notes 2026-03-11 20:24:20 +08:00
jialin 6f2c1daa41 feat: display provider models 2026-03-11 20:00:16 +08:00
jialin 353ee3a9ae fix: route target can be set 0 2026-03-10 19:06:49 +08:00
jialin 972e147fdf fix: yaml edior height 2026-03-10 19:06:49 +08:00
jialin 73e52fe334 chore: add hint for creating backend 2026-03-09 15:48:16 +08:00
jialin cf521ad79e fix: default_env field not display in backend editing yaml 2026-03-02 10:21:21 +08:00
jialin 76a3e5cfcf fix: custom config overwrite input value 2026-02-25 10:42:06 +08:00
jialin 200d63e5fc fix: add custom config in get-models, test-model 2026-02-25 10:28:31 +08:00
jialin 0aeefb1217 fix: allow deleting version for community backend 2026-02-24 17:23:42 +08:00
jialin 7dfeb859e2 refactor: query dashboard data 2026-02-24 16:33:28 +08:00
jialin 5151f26b9e fix: miss openaiCustomUrl in get-models payload 2026-02-24 15:36:41 +08:00
jialin 053e5cca86 chore: markdown for community backend detail 2026-02-24 11:37:22 +08:00
jialin 89765c19c4 fix: copy multi lines when == 2026-02-24 11:07:53 +08:00
jialin 0824f0c13b fix: provider proxy_url set null when disabled 2026-02-24 11:07:53 +08:00
jialin 04092c3fa5 fix: openaicustomurl do not display 2026-02-24 11:07:53 +08:00
68 changed files with 952 additions and 280 deletions
+45 -27
View File
@@ -1,13 +1,7 @@
import { CheckCircleFilled, CopyOutlined } from '@ant-design/icons'; import { CheckCircleFilled, CopyOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, message, Tooltip } from 'antd'; import { Button, message, Tooltip } from 'antd';
import React, { import React, { useEffect, useMemo, useRef, useState } from 'react';
useCallback,
useEffect,
useMemo,
useRef,
useState
} from 'react';
import AutoTooltip from '../auto-tooltip'; import AutoTooltip from '../auto-tooltip';
type CopyButtonProps = { type CopyButtonProps = {
@@ -55,39 +49,63 @@ const CopyButton: React.FC<CopyButtonProps> = ({
}; };
/** /**
* fallbackexecCommandold Safari / NON-HTTPS * Modern clipboard API (works in secure contexts: HTTPS or localhost)
*/ */
const legacyCopy = (value: string) => { const asyncCopy = async (value: string): Promise<boolean> => {
const textarea = document.createElement('textarea');
textarea.value = value;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try { try {
document.execCommand('copy'); await navigator.clipboard.writeText(value);
return true; return true;
} catch { } catch (error) {
return false; return false;
} finally {
document.body.removeChild(textarea);
} }
}; };
const handleCopy = useCallback(async () => { /**
* Fallback: execCommand with copy event listener
* More reliable than textarea selection method
*/
const execCopy = (value: string): boolean => {
let copySuccess = false;
const onCopy = (event: ClipboardEvent) => {
event.stopPropagation();
event.preventDefault();
event.clipboardData?.clearData();
event.clipboardData?.setData('text/plain', value);
copySuccess = true;
};
try { try {
if (navigator.clipboard?.writeText) { document.addEventListener('copy', onCopy, { capture: true });
await navigator.clipboard.writeText(text); document.execCommand('copy');
} else { return copySuccess;
const success = legacyCopy(text); } catch (error) {
if (!success) throw new Error('legacy copy failed'); return false;
} finally {
document.removeEventListener('copy', onCopy, { capture: true });
}
};
const handleCopy = async () => {
try {
// Try modern clipboard API first
if (await asyncCopy(text)) {
setCopied(true);
return;
} }
// Fallback to execCommand method
if (execCopy(text)) {
setCopied(true); setCopied(true);
} catch { return;
}
// Both methods failed
throw new Error('Copy failed');
} catch (error) {
message.error(intl.formatMessage({ id: 'common.copy.fail' }) as string); message.error(intl.formatMessage({ id: 'common.copy.fail' }) as string);
} }
}, [text, intl]); };
const tipTitle = useMemo(() => { const tipTitle = useMemo(() => {
if (copied) { if (copied) {
+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 };
}); });
@@ -83,6 +83,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
open={open} open={open}
title={intl.formatMessage({ id: 'resources.table.key.tips' })} title={intl.formatMessage({ id: 'resources.table.key.tips' })}
> >
<span>
<SealInput.Input <SealInput.Input
disabled={disabled} disabled={disabled}
checkStatus="success" checkStatus="success"
@@ -92,6 +93,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
onBlur={(e: any) => handleKeyOnBlur(e, 'key')} onBlur={(e: any) => handleKeyOnBlur(e, 'key')}
onPaste={onPaste} onPaste={onPaste}
></SealInput.Input> ></SealInput.Input>
</span>
</Tooltip> </Tooltip>
)} )}
</div> </div>
+31 -24
View File
@@ -3,6 +3,7 @@ import { isNotEmptyValue } from '@/utils/index';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import type { CascaderAutoProps } from 'antd'; import type { CascaderAutoProps } from 'antd';
import { Cascader, Empty, Form } from 'antd'; import { Cascader, Empty, Form } from 'antd';
import classNames from 'classnames';
import _, { cloneDeep } from 'lodash'; import _, { cloneDeep } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import AutoTooltip from '../auto-tooltip'; import AutoTooltip from '../auto-tooltip';
@@ -10,28 +11,6 @@ import { SealFormItemProps } from './types';
import Wrapper from './wrapper'; import Wrapper from './wrapper';
import SelectWrapper from './wrapper/select'; import SelectWrapper from './wrapper/select';
const tag = (props: any) => {
if (props.isMaxTag) {
return props.label?.slice(0, -3);
}
const parent = _.split(props.value, '__RC_CASCADER_SPLIT__')?.[0];
return `${parent} / ${props?.label}`;
};
const renderTag = (props: any) => {
return (
<AutoTooltip
closable={props.closable}
onClose={props.onClose}
maxWidth={240}
style={{ marginRight: 4 }}
filled
>
{tag(props)}
</AutoTooltip>
);
};
const OptionNodes = (props: { const OptionNodes = (props: {
data: any; data: any;
notFoundContent?: React.ReactNode; notFoundContent?: React.ReactNode;
@@ -100,7 +79,9 @@ const SealCascader: React.FC<
alwaysFocus = false, alwaysFocus = false,
optionNode, optionNode,
notFoundContent, notFoundContent,
size = 'middle',
tagRender, tagRender,
displayRender,
...rest ...rest
} = props; } = props;
const intl = useIntl(); const intl = useIntl();
@@ -172,10 +153,34 @@ const SealCascader: React.FC<
props.onOpenChange?.(open); props.onOpenChange?.(open);
}; };
const tag = (props: any) => {
if (props.isMaxTag) {
return props.label?.slice(0, -3);
}
const parent = _.split(props.value, '__RC_CASCADER_SPLIT__')?.[0];
return displayRender ? props.label : `${parent} / ${props?.label}`;
};
const renderTag = (props: any) => {
return (
<AutoTooltip
closable={props.closable}
onClose={props.onClose}
maxWidth={240}
style={{ marginRight: 4 }}
filled
>
{tag(props)}
</AutoTooltip>
);
};
return ( return (
<SelectWrapper> <SelectWrapper>
<Wrapper <Wrapper
className="seal-select-wrapper" className={classNames('seal-select-wrapper', {
'seal-cascader-wrapper-small': size === 'small'
})}
classList={visible ? 'dropdown-visible' : ''} classList={visible ? 'dropdown-visible' : ''}
status={status} status={status}
label={label} label={label}
@@ -188,6 +193,7 @@ const SealCascader: React.FC<
<Cascader <Cascader
{...rest} {...rest}
placeholder={placeholder} placeholder={placeholder}
className={size === 'small' ? 'seal-cascader-small' : ''}
suffixIcon={<IconFont type="icon-down"></IconFont>} suffixIcon={<IconFont type="icon-down"></IconFont>}
optionRender={(data) => ( optionRender={(data) => (
<OptionNodes <OptionNodes
@@ -196,7 +202,8 @@ const SealCascader: React.FC<
optionNode={optionNode} optionNode={optionNode}
></OptionNodes> ></OptionNodes>
)} )}
tagRender={tagRender ?? renderTag} tagRender={tagRender || renderTag}
displayRender={displayRender}
ref={inputRef} ref={inputRef}
options={children ? null : _options} options={children ? null : _options}
onFocus={handleOnFocus} onFocus={handleOnFocus}
+2 -2
View File
@@ -179,8 +179,8 @@ const SimpleSelect: React.FC<SelectProps & { ref?: any; showTags?: boolean }> =
closable={props.closable} closable={props.closable}
onClose={props.onClose} onClose={props.onClose}
style={{ style={{
height: 24, height: 22,
backgroundColor: 'var(--ant-color-fill-tertiary)', backgroundColor: 'var(--ant-color-fill-secondary)',
fontSize: 'var(--ant-font-size)' fontSize: 'var(--ant-font-size)'
}} }}
className="flex-center" className="flex-center"
@@ -146,6 +146,9 @@ const SelectWrapper = styled.div`
margin-inline-start: 0 !important; margin-inline-start: 0 !important;
} }
} }
&.seal-cascader-small {
height: 40px;
}
.ant-select-input { .ant-select-input {
height: ${INPUTHEIGHT}px !important; height: ${INPUTHEIGHT}px !important;
@@ -190,6 +193,21 @@ const SelectWrapper = styled.div`
} }
} }
} }
&.seal-cascader-wrapper-small {
height: 40px;
.cascader-popup-wrapper {
top: 39px !important;
}
.ant-select-input {
height: 36px !important;
}
.ant-select {
padding-inline: 12px !important;
}
.__wrapper__.no-label .ant-select.ant-cascader .ant-select-placeholder {
top: 50% !important;
}
}
} }
`; `;
+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
View File
@@ -2,6 +2,7 @@ export default {
'apikeys.title': 'API Keys', 'apikeys.title': 'API Keys',
'apikeys.table.apikeys': 'keys', 'apikeys.table.apikeys': 'keys',
'apikeys.button.create': 'Add API Key', 'apikeys.button.create': 'Add API Key',
'apikeys.button.edit': 'Edit API Key',
'apikeys.title.save': 'Save API Key', 'apikeys.title.save': 'Save API Key',
'apikeys.form.expiretime': 'Expiration', 'apikeys.form.expiretime': 'Expiration',
'apikeys.form.apikey': 'API Key', 'apikeys.form.apikey': 'API Key',
+3 -1
View File
@@ -44,5 +44,7 @@ export default {
'backend.filter.source': 'Filter by source', 'backend.filter.source': 'Filter by source',
'backend.add.custom': 'Custom', 'backend.add.custom': 'Custom',
'backend.add.community': 'Community', 'backend.add.community': 'Community',
'backend.community.title': 'Community Backend Marketplace' 'backend.community.title': 'Community Backend Marketplace',
'backend.form.add.hint':
'To use a different version of a built-in backend (e.g., vLLM, SGLang, MindIE), please add a new version to the existing backend instead of adding a custom backend.'
}; };
+4 -1
View File
@@ -88,5 +88,8 @@ export default {
'benchmark.form.nonLlmModel.tips': 'benchmark.form.nonLlmModel.tips':
'Benchmarking currently only supports LLM models', 'Benchmarking currently only supports LLM models',
'benchmark.detail.result.duration': 'Duration', 'benchmark.detail.result.duration': 'Duration',
'benchmark.detail.result.basic': 'Basic' 'benchmark.detail.result.basic': 'Basic',
'benchmark.form.profile.ShareGPT': 'ShareGPT',
'benchmark.form.profile.ShareGPT.tips':
'Measures maximum throughput using real conversational data. Suitable for GPU and model performance benchmarking.'
}; };
+3 -1
View File
@@ -132,5 +132,7 @@ export default {
'clusters.addworker.theadNotes': 'clusters.addworker.theadNotes':
'If the <span class="bold-text">/usr/local/PPU_SDK</span> directory does not exist, please create a symbolic link pointing to the T-Head PPU SDK installed path: <span class="bold-text">ln -s /path/to/PPU_SDK /usr/local/PPU_SDK</span>.', 'If the <span class="bold-text">/usr/local/PPU_SDK</span> directory does not exist, please create a symbolic link pointing to the T-Head PPU SDK installed path: <span class="bold-text">ln -s /path/to/PPU_SDK /usr/local/PPU_SDK</span>.',
'clusters.addworker.theadNotes-02': 'clusters.addworker.theadNotes-02':
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.' 'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
'clusters.addworker.nvidiaNotes':
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.'
}; };
+15 -1
View File
@@ -23,5 +23,19 @@ export default {
'providers.form.rules.tokens': 'Please enter a valid API Key', 'providers.form.rules.tokens': 'Please enter a valid API Key',
'providers.form.rules.model': 'Please select a model', 'providers.form.rules.model': 'Please select a model',
'providers.form.model.duplicate': 'Duplicate model exists', 'providers.form.model.duplicate': 'Duplicate model exists',
'providers.table.registerRoute': 'Register Route' 'providers.table.registerRoute': 'Register Route',
'providers.form.azureServiceUrl': 'Azure OpenAI Service URL',
'providers.form.ollamaServerHost': 'Ollama Server Host',
'providers.form.ollamaServerPort': 'Ollama Server Port',
'providers.form.hunyuanAuthId': 'Hunyuan Auth ID',
'providers.form.hunyuanAuthKey': 'Hunyuan Auth Key',
'providers.form.cloudflareAccountId': 'Cloudflare Account ID',
'providers.form.targetLang': 'Target Language',
'providers.form.modelVersion': 'Model Version',
'providers.form.tritonDomain': 'Triton Server Domain',
'providers.form.modelVersion.tips':
'Specifies the model version used in Triton Server.',
'providers.form.tritonDomain.tips':
'The domain used to send requests to the Triton Server deployment.',
'providers.form.awsRegion': 'AWS Region'
}; };
+1
View File
@@ -2,6 +2,7 @@ export default {
'apikeys.title': 'APIキー', 'apikeys.title': 'APIキー',
'apikeys.table.apikeys': 'キー', 'apikeys.table.apikeys': 'キー',
'apikeys.button.create': '新しいAPIキーを作成', 'apikeys.button.create': '新しいAPIキーを作成',
'apikeys.button.edit': 'APIキーを編集',
'apikeys.title.save': 'APIキーを保存', 'apikeys.title.save': 'APIキーを保存',
'apikeys.form.expiretime': '有効期限', 'apikeys.form.expiretime': '有効期限',
'apikeys.form.apikey': 'APIキー', 'apikeys.form.apikey': 'APIキー',
+3 -1
View File
@@ -44,5 +44,7 @@ export default {
'backend.filter.source': 'Filter by source', 'backend.filter.source': 'Filter by source',
'backend.add.custom': 'Custom', 'backend.add.custom': 'Custom',
'backend.add.community': 'Community', 'backend.add.community': 'Community',
'backend.community.title': 'Community Backend Marketplace' 'backend.community.title': 'Community Backend Marketplace',
'backend.form.add.hint':
'To use a different version of a built-in backend (e.g., vLLM, SGLang, MindIE), please add a new version to the existing backend instead of adding a custom backend.'
}; };
+4 -1
View File
@@ -88,5 +88,8 @@ export default {
'benchmark.form.nonLlmModel.tips': 'benchmark.form.nonLlmModel.tips':
'Benchmarking currently only supports LLM models', 'Benchmarking currently only supports LLM models',
'benchmark.detail.result.duration': 'Duration', 'benchmark.detail.result.duration': 'Duration',
'benchmark.detail.result.basic': 'Basic' 'benchmark.detail.result.basic': 'Basic',
'benchmark.form.profile.ShareGPT': 'ShareGPT',
'benchmark.form.profile.ShareGPT.tips':
'Measures maximum throughput using real conversational data. Suitable for GPU and model performance benchmarking.'
}; };
+5 -2
View File
@@ -132,7 +132,9 @@ export default {
'clusters.addworker.theadNotes': 'clusters.addworker.theadNotes':
'If the <span class="bold-text>/usr/local/PPU_SDK</span> directory does not exist, please create a symbolic link pointing to the T-Head PPU SDK installed path: <span class="bold-text>ln -s /path/to/PPU_SDK /usr/local/PPU_SDK</span>', 'If the <span class="bold-text>/usr/local/PPU_SDK</span> directory does not exist, please create a symbolic link pointing to the T-Head PPU SDK installed path: <span class="bold-text>ln -s /path/to/PPU_SDK /usr/local/PPU_SDK</span>',
'clusters.addworker.theadNotes-02': 'clusters.addworker.theadNotes-02':
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.' 'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
'clusters.addworker.nvidiaNotes':
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -235,5 +237,6 @@ export default {
// 93. 'clusters.create.k8sTips2': 'You can also skip this step and register it later from the cluster list.', // 93. 'clusters.create.k8sTips2': 'You can also skip this step and register it later from the cluster list.',
// 94. 'clusters.create.steps.configure': 'Configure', // 94. 'clusters.create.steps.configure': 'Configure',
// 99. 'clusters.addworker.theadNotes': 'If the <span class="bold-text>/usr/local/PPU_SDK</span> directory does not exist, please create a symbolic link pointing to the T-Head PPU SDK installed path: <span class="bold-text>ln -s /path/to/PPU_SDK /usr/local/PPU_SDK</span>', // 99. 'clusters.addworker.theadNotes': 'If the <span class="bold-text>/usr/local/PPU_SDK</span> directory does not exist, please create a symbolic link pointing to the T-Head PPU SDK installed path: <span class="bold-text>ln -s /path/to/PPU_SDK /usr/local/PPU_SDK</span>',
// 100. 'clusters.addworker.theadNotes-02': 'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.' // 100. 'clusters.addworker.theadNotes-02': 'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
// 101. 'clusters.addworker.nvidiaNotes': 'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+15 -1
View File
@@ -23,5 +23,19 @@ export default {
'providers.form.rules.tokens': 'Please enter a valid API Key', 'providers.form.rules.tokens': 'Please enter a valid API Key',
'providers.form.rules.model': 'Please select a model', 'providers.form.rules.model': 'Please select a model',
'providers.form.model.duplicate': 'Duplicate model exists', 'providers.form.model.duplicate': 'Duplicate model exists',
'providers.table.registerRoute': 'Register Route' 'providers.table.registerRoute': 'Register Route',
'providers.form.azureServiceUrl': 'Azure OpenAI Service URL',
'providers.form.ollamaServerHost': 'Ollama Server Host',
'providers.form.ollamaServerPort': 'Ollama Server Port',
'providers.form.hunyuanAuthId': 'Hunyuan Auth ID',
'providers.form.hunyuanAuthKey': 'Hunyuan Auth Key',
'providers.form.cloudflareAccountId': 'Cloudflare Account ID',
'providers.form.targetLang': 'Target Language',
'providers.form.modelVersion': 'Model Version',
'providers.form.tritonDomain': 'Triton Server Domain',
'providers.form.modelVersion.tips':
'Specifies the model version used in Triton Server.',
'providers.form.tritonDomain.tips':
'The domain used to send requests to the Triton Server deployment.',
'providers.form.awsRegion': 'AWS Region'
}; };
+1
View File
@@ -2,6 +2,7 @@ export default {
'apikeys.title': 'API-ключи', 'apikeys.title': 'API-ключи',
'apikeys.table.apikeys': 'Ключи', 'apikeys.table.apikeys': 'Ключи',
'apikeys.button.create': 'Создать API-ключ', 'apikeys.button.create': 'Создать API-ключ',
'apikeys.button.edit': 'Редактировать API-ключ',
'apikeys.title.save': 'Сохранить API-ключ', 'apikeys.title.save': 'Сохранить API-ключ',
'apikeys.form.expiretime': 'Срок действия', 'apikeys.form.expiretime': 'Срок действия',
'apikeys.form.apikey': 'API-ключ', 'apikeys.form.apikey': 'API-ключ',
+4 -1
View File
@@ -44,7 +44,9 @@ export default {
'backend.filter.source': 'Filter by source', 'backend.filter.source': 'Filter by source',
'backend.add.custom': 'Custom', 'backend.add.custom': 'Custom',
'backend.add.community': 'Community', 'backend.add.community': 'Community',
'backend.community.title': 'Community Backend Marketplace' 'backend.community.title': 'Community Backend Marketplace',
'backend.form.add.hint':
'To use a different version of a built-in backend (e.g., vLLM, SGLang, MindIE), please add a new version to the existing backend instead of adding a custom backend.'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -54,4 +56,5 @@ export default {
// 4. 'backend.add.custom': 'Custom', // 4. 'backend.add.custom': 'Custom',
// 5. 'backend.add.community': 'Community', // 5. 'backend.add.community': 'Community',
// 6. 'backend.community.title': 'Community Backend Marketplace' // 6. 'backend.community.title': 'Community Backend Marketplace'
// 7. 'backend.form.add.hint': 'To use a different version of a built-in backend (e.g., vLLM, SGLang, MindIE), please add a new version to the existing backend instead of adding a custom backend.'
// ========== End of To-Do List ========== // ========== End of To-Do List ==========
+4 -1
View File
@@ -88,5 +88,8 @@ export default {
'benchmark.form.nonLlmModel.tips': 'benchmark.form.nonLlmModel.tips':
'Benchmarking currently only supports LLM models', 'Benchmarking currently only supports LLM models',
'benchmark.detail.result.duration': 'Duration', 'benchmark.detail.result.duration': 'Duration',
'benchmark.detail.result.basic': 'Basic' 'benchmark.detail.result.basic': 'Basic',
'benchmark.form.profile.ShareGPT': 'ShareGPT',
'benchmark.form.profile.ShareGPT.tips':
'Measures maximum throughput using real conversational data. Suitable for GPU and model performance benchmarking.'
}; };
+4 -1
View File
@@ -133,7 +133,9 @@ export default {
'clusters.addworker.theadNotes': 'clusters.addworker.theadNotes':
'If the <span class="bold-text>/usr/local/PPU_SDK</span> directory does not exist, please create a symbolic link pointing to the T-Head PPU SDK installed path: <span class="bold-text>ln -s /path/to/PPU_SDK /usr/local/PPU_SDK</span>', 'If the <span class="bold-text>/usr/local/PPU_SDK</span> directory does not exist, please create a symbolic link pointing to the T-Head PPU SDK installed path: <span class="bold-text>ln -s /path/to/PPU_SDK /usr/local/PPU_SDK</span>',
'clusters.addworker.theadNotes-02': 'clusters.addworker.theadNotes-02':
'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.' 'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.',
'clusters.addworker.nvidiaNotes':
'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.'
}; };
// ========== To-Do: Translate Keys (Remove After Translation) ========== // ========== To-Do: Translate Keys (Remove After Translation) ==========
@@ -149,4 +151,5 @@ export default {
// 10. 'clusters.addworker.theadNotes': 'If the <span class="bold-text>/usr/local/PPU_SDK</span> directory does not exist, please create a symbolic link pointing to the T-Head PPU SDK installed path: <span class="bold-text>ln -s /path/to/PPU_SDK /usr/local/PPU_SDK</span>', // 10. 'clusters.addworker.theadNotes': 'If the <span class="bold-text>/usr/local/PPU_SDK</span> directory does not exist, please create a symbolic link pointing to the T-Head PPU SDK installed path: <span class="bold-text>ln -s /path/to/PPU_SDK /usr/local/PPU_SDK</span>',
// 11. 'clusters.addworker.theadNotes-02': 'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.' // 11. 'clusters.addworker.theadNotes-02': 'T-Head PPU uses the Container Device Interface (CDI) for device injection and requires the <span class="bold-text">/var/run/cdi</span> directory to be available for CDI generation.'
// 12. 'clusters.addworker.metaxNotes': `If the <span class="bold-text">/opt/mxdriver</span> or <span class="bold-text">/opt/maca</span> directory does not exist, create a symbolic link to the MetaX driver and SDK installation path: <span class="desc-fill">ln -s /path/to/mxdriver /opt/mxdriver</span><span class="desc-fill">ln -s /path/to/maca /opt/maca</span>.`, // 12. 'clusters.addworker.metaxNotes': `If the <span class="bold-text">/opt/mxdriver</span> or <span class="bold-text">/opt/maca</span> directory does not exist, create a symbolic link to the MetaX driver and SDK installation path: <span class="desc-fill">ln -s /path/to/mxdriver /opt/mxdriver</span><span class="desc-fill">ln -s /path/to/maca /opt/maca</span>.`,
// 13. 'clusters.addworker.nvidiaNotes': 'The built-in inference backends in GPUStack v2.1 require <span class="bold-text">CUDA 12.6+</span>. Please ensure your NVIDIA driver version is <span class="bold-text">560</span> or newer.'
// ================================================================ // ================================================================
+15 -1
View File
@@ -23,5 +23,19 @@ export default {
'providers.form.rules.tokens': 'Please enter a valid API Key', 'providers.form.rules.tokens': 'Please enter a valid API Key',
'providers.form.rules.model': 'Please select a model', 'providers.form.rules.model': 'Please select a model',
'providers.form.model.duplicate': 'Duplicate model exists', 'providers.form.model.duplicate': 'Duplicate model exists',
'providers.table.registerRoute': 'Register Route' 'providers.table.registerRoute': 'Register Route',
'providers.form.azureServiceUrl': 'Azure OpenAI Service URL',
'providers.form.ollamaServerHost': 'Ollama Server Host',
'providers.form.ollamaServerPort': 'Ollama Server Port',
'providers.form.hunyuanAuthId': 'Hunyuan Auth ID',
'providers.form.hunyuanAuthKey': 'Hunyuan Auth Key',
'providers.form.cloudflareAccountId': 'Cloudflare Account ID',
'providers.form.targetLang': 'Target Language',
'providers.form.modelVersion': 'Model Version',
'providers.form.tritonDomain': 'Triton Server Domain',
'providers.form.modelVersion.tips':
'Specifies the model version used in Triton Server.',
'providers.form.tritonDomain.tips':
'The domain used to send requests to the Triton Server deployment.',
'providers.form.awsRegion': 'AWS Region'
}; };
+1
View File
@@ -2,6 +2,7 @@ export default {
'apikeys.title': 'API 密钥', 'apikeys.title': 'API 密钥',
'apikeys.table.apikeys': '密钥', 'apikeys.table.apikeys': '密钥',
'apikeys.button.create': '添加 API 密钥', 'apikeys.button.create': '添加 API 密钥',
'apikeys.button.edit': '编辑 API 密钥',
'apikeys.title.save': '保存 API 密钥', 'apikeys.title.save': '保存 API 密钥',
'apikeys.form.expiretime': '过期时间', 'apikeys.form.expiretime': '过期时间',
'apikeys.form.apikey': 'API 密钥', 'apikeys.form.apikey': 'API 密钥',
+3 -1
View File
@@ -41,5 +41,7 @@ export default {
'backend.filter.source': '按来源过滤', 'backend.filter.source': '按来源过滤',
'backend.add.custom': '自定义', 'backend.add.custom': '自定义',
'backend.add.community': '社区', 'backend.add.community': '社区',
'backend.community.title': '社区后端市场' 'backend.community.title': '社区后端市场',
'backend.form.add.hint':
'如果需要使用内置后端(如 vLLM、SGLang、MindIE)的其他版本,请在对应后端中添加新版本,而不是添加自定义后端。'
}; };
+4 -1
View File
@@ -87,5 +87,8 @@ export default {
'benchmark.table.export.results': '导出结果', 'benchmark.table.export.results': '导出结果',
'benchmark.form.nonLlmModel.tips': '基准测试目前仅支持 LLM 模型', 'benchmark.form.nonLlmModel.tips': '基准测试目前仅支持 LLM 模型',
'benchmark.detail.result.duration': '耗时', 'benchmark.detail.result.duration': '耗时',
'benchmark.detail.result.basic': '基础信息' 'benchmark.detail.result.basic': '基础信息',
'benchmark.form.profile.ShareGPT': 'ShareGPT',
'benchmark.form.profile.ShareGPT.tips':
'用于测量基于真实对话数据的最大吞吐量,适合进行 GPU 与模型性能基准测试。'
}; };
+3 -1
View File
@@ -126,5 +126,7 @@ export default {
'clusters.addworker.theadNotes': 'clusters.addworker.theadNotes':
'如果 <span class="bold-text">/usr/local/PPU_SDK</span> 目录不存在,请创建一个指向已安装平头哥(T-Head)PPU SDK 路径的符号链接:<span class="bold-text">ln -s /path/to/PPU_SDK /usr/local/PPU_SDK</span>。', '如果 <span class="bold-text">/usr/local/PPU_SDK</span> 目录不存在,请创建一个指向已安装平头哥(T-Head)PPU SDK 路径的符号链接:<span class="bold-text">ln -s /path/to/PPU_SDK /usr/local/PPU_SDK</span>。',
'clusters.addworker.theadNotes-02': 'clusters.addworker.theadNotes-02':
'平头哥(T-Head)PPU 使用容器设备接口(CDI)进行设备注入,因此需要确保 <span class="bold-text">/var/run/cdi</span> 目录可用以生成 CDI。' '平头哥(T-Head)PPU 使用容器设备接口(CDI)进行设备注入,因此需要确保 <span class="bold-text">/var/run/cdi</span> 目录可用以生成 CDI。',
'clusters.addworker.nvidiaNotes':
'GPUStack v2.1 内置推理后端依赖 <span class="bold-text">CUDA 12.6</span> 及以上版本,请确保 NVIDIA 驱动版本为 <span class="bold-text">560</span> 或以上。'
}; };
+13 -1
View File
@@ -23,5 +23,17 @@ export default {
'providers.form.rules.tokens': '请输入有效的 API Key', 'providers.form.rules.tokens': '请输入有效的 API Key',
'providers.form.rules.model': '请选择模型', 'providers.form.rules.model': '请选择模型',
'providers.form.model.duplicate': '存在相同的模型', 'providers.form.model.duplicate': '存在相同的模型',
'providers.table.registerRoute': '注册路由' 'providers.table.registerRoute': '注册路由',
'providers.form.azureServiceUrl': 'Azure OpenAI 服务 URL',
'providers.form.ollamaServerHost': 'Ollama 服务地址',
'providers.form.ollamaServerPort': 'Ollama 服务端口',
'providers.form.hunyuanAuthId': '混元认证 ID',
'providers.form.hunyuanAuthKey': '混元认证 Key',
'providers.form.cloudflareAccountId': 'Cloudflare 账号 ID',
'providers.form.targetLang': '翻译目标语言',
'providers.form.modelVersion': '模型版本',
'providers.form.modelVersion.tips': '用于指定 Triton Server 中的模型版本。',
'providers.form.tritonDomain': 'Triton Server 域名',
'providers.form.tritonDomain.tips': 'Triton Server 部署的指定请求的域名。',
'providers.form.awsRegion': 'AWS 区域'
}; };
@@ -30,6 +30,7 @@ const useScrollActiveChange = (options: {
return { return {
activeKey, activeKey,
collapseKeys, collapseKeys,
setCollapseKeys,
handleActiveChange, handleActiveChange,
handleOnCollapseChange, handleOnCollapseChange,
updateActiveKey updateActiveKey
+6 -4
View File
@@ -18,9 +18,7 @@ const { Text } = Typography;
loader.config({ monaco }); loader.config({ monaco });
const Container = styled.div<{ $minHeight: string | number }>` const Container = styled.div`
min-height: ${({ $minHeight }) =>
typeof $minHeight === 'number' ? `${$minHeight}px` : $minHeight};
position: relative; position: relative;
border: 1px solid var(--ant-color-border); border: 1px solid var(--ant-color-border);
border-radius: var(--ant-border-radius); border-radius: var(--ant-border-radius);
@@ -153,7 +151,11 @@ const YamlEditor: React.FC<ViewerProps> = forwardRef((props, ref) => {
}, [value]); }, [value]);
return ( return (
<Container $minHeight={height}> <Container
style={{
minHeight: height
}}
>
<EditorInner <EditorInner
ref={editorRef} ref={editorRef}
header={renderHeader()} header={renderHeader()}
+1 -1
View File
@@ -62,7 +62,7 @@ const APIKeys: React.FC = () => {
const handleEditKey = (record: ListItem) => { const handleEditKey = (record: ListItem) => {
setOpenAddModal({ setOpenAddModal({
open: true, open: true,
title: 'Edit API Key', title: intl.formatMessage({ id: 'apikeys.button.edit' }),
action: PageAction.EDIT, action: PageAction.EDIT,
currentData: record currentData: record
}); });
@@ -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>
)} )}
+26 -5
View File
@@ -1,3 +1,4 @@
import AlertBlockInfo from '@/components/alert-info/block';
import IconFont from '@/components/icon-font'; import IconFont from '@/components/icon-font';
import ModalFooter from '@/components/modal-footer'; import ModalFooter from '@/components/modal-footer';
import GSDrawer from '@/components/scroller-modal/gs-drawer'; import GSDrawer from '@/components/scroller-modal/gs-drawer';
@@ -59,6 +60,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
const [activeKey, setActiveKey] = useState<string>('form'); const [activeKey, setActiveKey] = useState<string>('form');
const [yamlContent, setYamlContent] = useState<string>(''); const [yamlContent, setYamlContent] = useState<string>('');
const [formContent, setFormContent] = useState<FormData>({} as FormData); const [formContent, setFormContent] = useState<FormData>({} as FormData);
const alertRef = useRef<HTMLDivElement>(null);
const showVersionCustomSuffix = const showVersionCustomSuffix =
currentData?.backend_source === BackendSourceValueMap.BUILTIN || currentData?.backend_source === BackendSourceValueMap.BUILTIN ||
@@ -145,11 +147,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 {
@@ -196,6 +194,15 @@ const AddModal: React.FC<AddModalProps> = (props) => {
} }
}, [action, currentData, open]); }, [action, currentData, open]);
const yamlHeight = useMemo(() => {
if (action !== PageAction.CREATE) {
return undefined;
}
const baseHeight = 260;
const alertHeight = alertRef.current?.offsetHeight || 0;
return `calc(100vh - ${baseHeight + alertHeight}px)`;
}, [action, activeKey]);
return ( return (
<GSDrawer <GSDrawer
title={title} title={title}
@@ -248,11 +255,24 @@ const AddModal: React.FC<AddModalProps> = (props) => {
} }
}} }}
footer={ footer={
<>
{action === PageAction.CREATE && open && (
<div style={{ marginInline: 24, paddingTop: 8 }} ref={alertRef}>
<AlertBlockInfo
type="warning"
contentStyle={{ paddingInline: 0 }}
message={intl.formatMessage({
id: 'backend.form.add.hint'
})}
></AlertBlockInfo>
</div>
)}
<ModalFooter <ModalFooter
onCancel={onClose} onCancel={onClose}
onOk={onOk} onOk={onOk}
style={ModalFooterStyle} style={ModalFooterStyle}
></ModalFooter> ></ModalFooter>
</>
} }
> >
<Tabs <Tabs
@@ -277,6 +297,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
label: intl.formatMessage({ id: 'backend.mode.yaml' }), label: intl.formatMessage({ id: 'backend.mode.yaml' }),
children: ( children: (
<ImportYAML <ImportYAML
height={yamlHeight}
actionStatus={{ actionStatus={{
action: action, action: action,
backendSource: backendSource backendSource: backendSource
@@ -19,12 +19,13 @@ interface ImportYAMLProps {
action: PageActionType; action: PageActionType;
backendSource: string; backendSource: string;
}; };
height?: string | number;
content?: string; content?: string;
onSubmit?: (content: string) => void; onSubmit?: (content: string) => void;
} }
const ImportYAML: React.FC<ImportYAMLProps> = forwardRef( const ImportYAML: React.FC<ImportYAMLProps> = forwardRef(
({ actionStatus, content = '' }, ref) => { ({ actionStatus, content = '', height }, ref) => {
const intl = useIntl(); const intl = useIntl();
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const [fileContent, setFileContent] = useState<string>( const [fileContent, setFileContent] = useState<string>(
@@ -109,7 +110,7 @@ const ImportYAML: React.FC<ImportYAMLProps> = forwardRef(
<YamlEditor <YamlEditor
ref={editorRef} ref={editorRef}
value={fileContent} value={fileContent}
height={'calc(100vh - 260px)'} height={height || 'calc(100vh - 260px)'}
validateMessage={error} validateMessage={error}
onUpload={(content) => { onUpload={(content) => {
setError(''); setError('');
+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
) => { ) => {
@@ -5,6 +5,7 @@ import { useIntl } from '@umijs/max';
import { Button, Input, Space } from 'antd'; import { Button, Input, Space } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React from 'react'; import React from 'react';
import { profileOptions } from '../config';
export interface RightActionsProps { export interface RightActionsProps {
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void; handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
@@ -67,12 +68,38 @@ const RightActions: React.FC<RightActionsProps> = ({
allowClear allowClear
onChange={handleGPUChange} onChange={handleGPUChange}
></Input> ></Input>
<BaseSelect
allowClear
placeholder={intl.formatMessage({
id: 'benchmark.table.filter.byProfile'
})}
style={{ width: 160 }}
options={[
...profileOptions,
{
label: 'backend.custom',
locale: true,
value: 'Custom'
}
].map((item) => ({
label: item.locale
? intl.formatMessage({ id: item.label })
: item.label,
value: item.value
}))}
onChange={(value, option) =>
handleQueryChange({
profile: value,
page: 1
})
}
></BaseSelect>
<BaseSelect <BaseSelect
allowClear allowClear
placeholder={intl.formatMessage({ placeholder={intl.formatMessage({
id: 'benchmark.table.filter.bymodel' id: 'benchmark.table.filter.bymodel'
})} })}
style={{ width: 200 }} style={{ width: 180 }}
options={modelOptions} options={modelOptions}
onChange={(value, option) => onChange={(value, option) =>
handleQueryChange({ handleQueryChange({
@@ -32,7 +32,7 @@ const PercentileResult: React.FC = () => {
render: (value: number) => round(value, 2) render: (value: number) => round(value, 2)
}, },
{ {
title: `${intl.formatMessage({ id: 'benchmark.detail.percentile.latency' })} (ms)`, title: `${intl.formatMessage({ id: 'benchmark.detail.percentile.latency' })} (s)`,
dataIndex: 'request_latency', dataIndex: 'request_latency',
render: (value: number) => round(value, 2) render: (value: number) => round(value, 2)
}, },
+8
View File
@@ -36,6 +36,7 @@ export const ProfileValueMap = {
ThroughputMedium: 'Throughput', ThroughputMedium: 'Throughput',
LongContextStress: 'Long Context', LongContextStress: 'Long Context',
GenerationHeavy: 'Generation Heavy', GenerationHeavy: 'Generation Heavy',
ShareGPT: 'ShareGPT',
Custom: 'Custom' Custom: 'Custom'
}; };
@@ -44,6 +45,7 @@ export const ProfileLabelMap = {
[ProfileValueMap.ThroughputMedium]: 'benchmark.form.profile.throughput', [ProfileValueMap.ThroughputMedium]: 'benchmark.form.profile.throughput',
[ProfileValueMap.LongContextStress]: 'benchmark.form.profile.longContext', [ProfileValueMap.LongContextStress]: 'benchmark.form.profile.longContext',
[ProfileValueMap.GenerationHeavy]: 'benchmark.form.profile.heavy', [ProfileValueMap.GenerationHeavy]: 'benchmark.form.profile.heavy',
[ProfileValueMap.ShareGPT]: 'benchmark.form.profile.ShareGPT',
[ProfileValueMap.Custom]: 'benchmark.form.profile.custom' [ProfileValueMap.Custom]: 'benchmark.form.profile.custom'
}; };
@@ -71,6 +73,12 @@ export const profileOptions = [
tips: 'benchmark.form.profile.heavy.tips', tips: 'benchmark.form.profile.heavy.tips',
value: ProfileValueMap.GenerationHeavy, value: ProfileValueMap.GenerationHeavy,
locale: true locale: true
},
{
label: 'benchmark.form.profile.ShareGPT',
tips: 'benchmark.form.profile.ShareGPT.tips',
value: ProfileValueMap.ShareGPT,
locale: true
} }
]; ];
+7 -1
View File
@@ -115,6 +115,7 @@ const ModelInstanceForm: React.FC = () => {
disabled: modelCategoriesMap.llm !== model.categories?.[0], disabled: modelCategoriesMap.llm !== model.categories?.[0],
id: model.id, id: model.id,
isLeaf: false, isLeaf: false,
ready_replicas: model.ready_replicas,
children: [] children: []
})); }));
@@ -123,15 +124,20 @@ const ModelInstanceForm: React.FC = () => {
} }
// preload instances for the first model // preload instances for the first model
const selectedllmModel = modelOptions.find((model) => !model.disabled); const selectedllmModel = modelOptions.find(
(model) => !model.disabled && model.ready_replicas > 0
);
if (!selectedllmModel) { if (!selectedllmModel) {
setModelList(modelOptions); setModelList(modelOptions);
return; return;
} }
const instanceList = await fetchInstanceList({ id: selectedllmModel.id }); const instanceList = await fetchInstanceList({ id: selectedllmModel.id });
const instanceOptions = instanceList.map((instance: any) => const instanceOptions = instanceList.map((instance: any) =>
renderInstance(instance) renderInstance(instance)
); );
if (selectedllmModel) { if (selectedllmModel) {
selectedllmModel.children = [...instanceOptions] as never[]; selectedllmModel.children = [...instanceOptions] as never[];
} }
@@ -143,7 +143,7 @@ const useColumnSettings = (options: {
title: renderTitle( title: renderTitle(
`${intl.formatMessage({ id: 'benchmark.detail.summary.latency' })}`, `${intl.formatMessage({ id: 'benchmark.detail.summary.latency' })}`,
{ {
subTitle: `${intl.formatMessage({ id: 'benchmark.table.avg' })} (ms)` subTitle: `${intl.formatMessage({ id: 'benchmark.table.avg' })} (s)`
} }
), ),
dataIndex: 'request_latency_mean', dataIndex: 'request_latency_mean',
@@ -138,6 +138,16 @@ const SupportedHardware: React.FC<SupportedHardwareProps> = ({
link: 'https://docs.gpustack.ai/latest/installation/requirements/#hygon-dcu', link: 'https://docs.gpustack.ai/latest/installation/requirements/#hygon-dcu',
icon: <ProviderImage src={hyponPNG} height={18} /> icon: <ProviderImage src={hyponPNG} height={18} />
}, },
{
label: intl.formatMessage({ id: 'vendor.metax' }),
hiddenTitle: true,
value: GPUDriverMap.METAX,
key: GPUDriverMap.METAX,
locale: false,
link: 'https://docs.gpustack.ai/latest/installation/requirements/#metax-gpu',
notes: AddWorkerDockerNotes[GPUDriverMap.METAX],
icon: <ProviderImage src={metaxLogo} height={20} />
},
{ {
label: intl.formatMessage({ id: 'vendor.moorthreads' }), label: intl.formatMessage({ id: 'vendor.moorthreads' }),
hiddenTitle: true, hiddenTitle: true,
@@ -171,17 +181,6 @@ const SupportedHardware: React.FC<SupportedHardwareProps> = ({
link: 'https://docs.gpustack.ai/latest/installation/requirements/#cambricon-mlu', link: 'https://docs.gpustack.ai/latest/installation/requirements/#cambricon-mlu',
icon: <ProviderImage src={CambriconPNG} height={24} /> icon: <ProviderImage src={CambriconPNG} height={24} />
}, },
{
label: intl.formatMessage({ id: 'vendor.metax' }),
hiddenTitle: true,
extra: intl.formatMessage({ id: 'common.tag.experimental' }),
value: GPUDriverMap.METAX,
key: GPUDriverMap.METAX,
locale: false,
link: 'https://docs.gpustack.ai/latest/installation/requirements/#metax-gpu',
notes: AddWorkerDockerNotes[GPUDriverMap.METAX],
icon: <ProviderImage src={metaxLogo} height={20} />
},
{ {
label: `${intl.formatMessage({ id: 'vendor.thead' })} `, label: `${intl.formatMessage({ id: 'vendor.thead' })} `,
hiddenTitle: true, hiddenTitle: true,
@@ -24,9 +24,10 @@ const ExportData: React.FC<{
result, result,
userList, userList,
modelList, modelList,
selectedModels,
query, query,
setQuery, setQuery,
handleExport, resetQuery,
handleDateChange, handleDateChange,
handleUsersChange, handleUsersChange,
handleModelsChange handleModelsChange
@@ -37,6 +38,21 @@ const ExportData: React.FC<{
disabledDate: false disabledDate: false
}); });
const getModelName = (record: any) => {
if (record.model_id) {
const children =
modelList.find((item) => item.value === 'deployments')?.children || [];
return (
children?.find((item) => item.value === record.model_id)?.label ||
record.model_id
);
}
const provider =
modelList.find((item) => item.value === record.provider_id)?.label ||
record.provider_id;
return `${provider} / ${record.model_name}`;
};
const exportTableColumns: TableColumnType[] = [ const exportTableColumns: TableColumnType[] = [
{ {
title: intl.formatMessage({ id: 'resources.table.index' }), title: intl.formatMessage({ id: 'resources.table.index' }),
@@ -63,12 +79,9 @@ const ExportData: React.FC<{
{ {
title: intl.formatMessage({ id: 'dashboard.usage.export.model' }), title: intl.formatMessage({ id: 'dashboard.usage.export.model' }),
dataIndex: 'model_id', dataIndex: 'model_id',
render: (text: string) => { render: (text: string, record: any) => {
return ( console.log('render model id: ', record, modelList);
<AutoTooltip ghost> return <AutoTooltip ghost>{getModelName(record)}</AutoTooltip>;
{modelList.find((item) => item.value === text)?.label || text}
</AutoTooltip>
);
} }
}, },
@@ -91,6 +104,7 @@ const ExportData: React.FC<{
width: 150 width: 150
} }
]; ];
const handleSubmit = () => { const handleSubmit = () => {
const fileName = `usage-data_${query.start_date || ''}_${query.end_date || ''}.xlsx`; const fileName = `usage-data_${query.start_date || ''}_${query.end_date || ''}.xlsx`;
exportJsonToExcel({ exportJsonToExcel({
@@ -111,13 +125,18 @@ const ExportData: React.FC<{
user_id: (value: string) => { user_id: (value: string) => {
return userList.find((item) => item.value === value)?.label || value; return userList.find((item) => item.value === value)?.label || value;
}, },
model_id: (value: string) => { model_id: (value: string, record: any) => {
return modelList.find((item) => item.value === value)?.label || value; return getModelName(record);
} }
} }
}); });
}; };
const handleOnCancel = () => {
onCancel?.();
resetQuery();
};
useEffect(() => { useEffect(() => {
if (open) { if (open) {
init(); init();
@@ -126,6 +145,7 @@ const ExportData: React.FC<{
start_date: dayjs().subtract(29, 'days').format('YYYY-MM-DD'), start_date: dayjs().subtract(29, 'days').format('YYYY-MM-DD'),
end_date: dayjs().format('YYYY-MM-DD'), end_date: dayjs().format('YYYY-MM-DD'),
model_ids: [], model_ids: [],
provider_model_names: [],
user_ids: [] user_ids: []
}); });
setResult({ setResult({
@@ -141,7 +161,7 @@ const ExportData: React.FC<{
title={intl.formatMessage({ id: 'dashboard.usage.export' })} title={intl.formatMessage({ id: 'dashboard.usage.export' })}
open={open} open={open}
centered={false} centered={false}
onCancel={onCancel} onCancel={handleOnCancel}
destroyOnHidden={true} destroyOnHidden={true}
closeIcon={true} closeIcon={true}
maskClosable={false} maskClosable={false}
@@ -153,7 +173,7 @@ const ExportData: React.FC<{
footer={ footer={
<ModalFooter <ModalFooter
onOk={handleSubmit} onOk={handleSubmit}
onCancel={onCancel} onCancel={handleOnCancel}
okText={intl.formatMessage({ id: 'common.button.export' })} okText={intl.formatMessage({ id: 'common.button.export' })}
></ModalFooter> ></ModalFooter>
} }
@@ -164,6 +184,8 @@ const ExportData: React.FC<{
query={query} query={query}
userList={userList} userList={userList}
modelList={modelList} modelList={modelList}
selectedModels={selectedModels}
cascaderWidth={360}
handleDateChange={handleDateChange} handleDateChange={handleDateChange}
handleUsersChange={handleUsersChange} handleUsersChange={handleUsersChange}
handleModelsChange={handleModelsChange} handleModelsChange={handleModelsChange}
@@ -171,7 +193,7 @@ const ExportData: React.FC<{
<Table <Table
columns={exportTableColumns} columns={exportTableColumns}
tableLayout={'auto'} tableLayout={'auto'}
style={{ width: '100%', marginTop: '16px' }} style={{ width: '100%', marginTop: '16px', minHeight: 300 }}
dataSource={result.data?.items || []} dataSource={result.data?.items || []}
loading={loading} loading={loading}
rowKey="id" rowKey="id"
@@ -1,4 +1,7 @@
import AutoTooltip from '@/components/auto-tooltip';
import SealCascader from '@/components/seal-form/seal-cascader';
import SimpleSelect from '@/components/seal-form/simple-select'; import SimpleSelect from '@/components/seal-form/simple-select';
import ProviderLogo from '@/pages/maas-provider/components/provider-logo';
import { DownloadOutlined } from '@ant-design/icons'; import { DownloadOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, DatePicker, Tooltip } from 'antd'; import { Button, DatePicker, Tooltip } from 'antd';
@@ -8,12 +11,26 @@ import styled from 'styled-components';
import { DASHBOARD_STATS_API } from '../../apis'; import { DASHBOARD_STATS_API } from '../../apis';
import useRangePickerPreset from '../../hooks/use-rangepicker-preset'; import useRangePickerPreset from '../../hooks/use-rangepicker-preset';
const OptionWrapper = styled.span`
display: flex;
align-items: center;
gap: 8px;
`;
const LabelWrapper = styled.div`
display: flex;
align-items: center;
gap: 8px;
`;
const DefaultDateConfig = { const DefaultDateConfig = {
maxRange: 60, maxRange: 60,
defaultRange: 29 defaultRange: 29
}; };
const FilterWrapper = styled.div` const FilterWrapper = styled.div`
position: relative;
z-index: 10;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@@ -36,8 +53,10 @@ interface FilterBarProps {
handleUsersChange: (value: any) => void; handleUsersChange: (value: any) => void;
handleModelsChange: (value: any) => void; handleModelsChange: (value: any) => void;
handleExport?: () => void; handleExport?: () => void;
selectedModels: string[][];
url: string; url: string;
disabledDate?: boolean; disabledDate?: boolean;
cascaderWidth?: number;
} }
const FilterBar: React.FC<FilterBarProps> = (props) => { const FilterBar: React.FC<FilterBarProps> = (props) => {
@@ -45,6 +64,8 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
query, query,
userList, userList,
modelList, modelList,
selectedModels,
cascaderWidth = 300,
handleDateChange, handleDateChange,
handleUsersChange, handleUsersChange,
handleModelsChange, handleModelsChange,
@@ -52,7 +73,6 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
url, url,
disabledDate disabledDate
} = props; } = props;
const { disabledRangeDaysDate, rangePresets } = useRangePickerPreset({ const { disabledRangeDaysDate, rangePresets } = useRangePickerPreset({
range: DefaultDateConfig.maxRange, range: DefaultDateConfig.maxRange,
disabledDate: disabledDate disabledDate: disabledDate
@@ -60,6 +80,50 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
const intl = useIntl(); const intl = useIntl();
const displayRender = (labels: any[], option: any) => {
return (
<AutoTooltip
ghost
maxWidth={150}
title={
<span>
{labels[0]} / {labels[1]}
</span>
}
>
{labels[0]} / {labels[1]}
</AutoTooltip>
);
};
const optionRender = (option: any) => {
const { data } = option;
if (!data.isParent) {
return <AutoTooltip ghost>{data.label}</AutoTooltip>;
}
if (data.providerType === 'deployments') {
return (
<OptionWrapper>
<ProviderLogo provider={data.providerType as string} />
<AutoTooltip ghost maxWidth={105}>
{intl.formatMessage({ id: 'menu.models.deployment' })}
</AutoTooltip>
</OptionWrapper>
);
}
return (
<OptionWrapper>
<ProviderLogo provider={data.providerType as string} />
<AutoTooltip ghost maxWidth={105}>
<span>{data.label}</span>
</AutoTooltip>
</OptionWrapper>
);
};
return ( return (
<FilterWrapper> <FilterWrapper>
<div className="selection"> <div className="selection">
@@ -89,19 +153,37 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
value={query.user_ids} value={query.user_ids}
onChange={handleUsersChange} onChange={handleUsersChange}
></SimpleSelect> ></SimpleSelect>
<SimpleSelect <SealCascader
allowClear
showSearch showSearch
mode="multiple" multiple={true}
options={modelList} onChange={handleModelsChange}
maxTagCount={0} classNames={{
popup: {
root: 'cascader-popup-wrapper gpu-selector'
}
}}
styles={{
root: {
width: cascaderWidth
},
popup: {
listItem: {
padding: '5px 10px'
}
}
}}
maxTagCount={1}
size="small"
placeholder={intl.formatMessage({ placeholder={intl.formatMessage({
id: 'dashboard.usage.selectmodel' id: 'dashboard.usage.selectmodel'
})} })}
value={query.model_ids} options={modelList}
style={{ maxWidth: 200, minWidth: 160 }} value={selectedModels}
onChange={handleModelsChange} showCheckedStrategy="SHOW_CHILD"
></SimpleSelect> displayRender={displayRender}
optionNode={optionRender}
getPopupContainer={(triggerNode) => triggerNode.parentNode}
></SealCascader>
{url === DASHBOARD_STATS_API && ( {url === DASHBOARD_STATS_API && (
<Tooltip title={intl.formatMessage({ id: 'common.button.export' })}> <Tooltip title={intl.formatMessage({ id: 'common.button.export' })}>
<Button icon={<DownloadOutlined />} onClick={handleExport}></Button> <Button icon={<DownloadOutlined />} onClick={handleExport}></Button>
@@ -16,6 +16,7 @@ import useUsageData from './use-usage-data';
const TitleWrapper = styled.div` const TitleWrapper = styled.div`
margin: 0; margin: 0;
font-weight: 700; font-weight: 700;
min-width: max-content;
`; `;
const UsageInner: FC<{ maxWidth: number }> = ({ maxWidth }) => { const UsageInner: FC<{ maxWidth: number }> = ({ maxWidth }) => {
@@ -27,6 +28,7 @@ const UsageInner: FC<{ maxWidth: number }> = ({ maxWidth }) => {
query, query,
userList, userList,
modelList, modelList,
selectedModels,
handleOnCancel, handleOnCancel,
init, init,
handleExport, handleExport,
@@ -109,6 +111,7 @@ const UsageInner: FC<{ maxWidth: number }> = ({ maxWidth }) => {
query={query} query={query}
userList={userList} userList={userList}
modelList={modelList} modelList={modelList}
selectedModels={selectedModels}
disabledDate={true} disabledDate={true}
handleDateChange={handleDateChange} handleDateChange={handleDateChange}
handleUsersChange={handleUsersChange} handleUsersChange={handleUsersChange}
@@ -1,5 +1,4 @@
import { queryModelsList } from '@/pages/llmodels/apis'; import useTargetSourceModels from '@/pages/model-routes/hooks/use-target-source-models';
import { ListItem as ModelListItem } from '@/pages/llmodels/config/types';
import { queryUsersList } from '@/pages/users/apis'; import { queryUsersList } from '@/pages/users/apis';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import _ from 'lodash'; import _ from 'lodash';
@@ -93,6 +92,7 @@ export default function useUseageData<T>(config: {
start_date: string; start_date: string;
end_date: string; end_date: string;
model_ids: number[]; model_ids: number[];
provider_model_names: string[];
user_ids: number[]; user_ids: number[];
}>({ }>({
start_date: dayjs() start_date: dayjs()
@@ -100,12 +100,14 @@ export default function useUseageData<T>(config: {
.format('YYYY-MM-DD'), .format('YYYY-MM-DD'),
end_date: dayjs().format('YYYY-MM-DD'), end_date: dayjs().format('YYYY-MM-DD'),
model_ids: [], model_ids: [],
user_ids: [] user_ids: [],
provider_model_names: []
}); });
const { sourceModels: modelList, fetchSourceModels } =
const [modelList, setModelList] = useState<Global.BaseOption<string>[]>([]); useTargetSourceModels();
const [userList, setUserList] = useState<Global.BaseOption<string>[]>([]); const [userList, setUserList] = useState<Global.BaseOption<string>[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [selectedModels, setSelectedModels] = useState<string[][]>([]);
const usageData = useMemo<{ const usageData = useMemo<{
requestTokenData: RequestTokenData; requestTokenData: RequestTokenData;
@@ -209,25 +211,6 @@ export default function useUseageData<T>(config: {
}; };
}, [result, url]); }, [result, url]);
const fetchModelsList = async () => {
try {
const params = {
page: -1
};
const response = await queryModelsList(params);
const list = _.map(response.items || [], (item: ModelListItem) => {
return {
label: item.name,
value: item.id
};
});
setModelList(list);
} catch (error) {
setModelList([]);
}
};
const fetchUsersList = async () => { const fetchUsersList = async () => {
try { try {
const params = { const params = {
@@ -301,19 +284,49 @@ export default function useUseageData<T>(config: {
}); });
fetchUsageData({ ...query, user_ids: value }); fetchUsageData({ ...query, user_ids: value });
}; };
const handleModelsChange = (value: number[]) => {
const generateModelsValue = (value: string[][]) => {
const modelIds = [] as number[];
const providerModelNames = [] as string[];
value.forEach((item: Array<string | number>) => {
if (item[0] === 'deployments') {
modelIds.push(item[1] as number);
} else {
providerModelNames.push(`${item[0]}:${item[1]}`);
}
});
return {
model_ids: modelIds,
provider_model_names: providerModelNames
};
};
const handleModelsChange = (value: string[][]) => {
setSelectedModels(value);
setQuery((pre) => { setQuery((pre) => {
return { return {
...pre, ...pre,
model_ids: value ...generateModelsValue(value)
}; };
}); });
fetchUsageData({ ...query, model_ids: value }); fetchUsageData({ ...query, ...generateModelsValue(value) });
};
const resetQuery = () => {
setQuery({
start_date: dayjs()
.subtract(DefaultDateConfig.defaultRange, 'days')
.format('YYYY-MM-DD'),
end_date: dayjs().format('YYYY-MM-DD'),
model_ids: [],
user_ids: [],
provider_model_names: []
});
setSelectedModels([]);
}; };
const init = () => { const init = () => {
fetchUsageData(query); fetchUsageData(query);
fetchModelsList(); fetchSourceModels();
fetchUsersList(); fetchUsersList();
}; };
@@ -325,6 +338,7 @@ export default function useUseageData<T>(config: {
userList, userList,
modelList, modelList,
query, query,
selectedModels,
setQuery, setQuery,
init, init,
setResult, setResult,
@@ -332,6 +346,7 @@ export default function useUseageData<T>(config: {
handleExport, handleExport,
handleDateChange, handleDateChange,
handleUsersChange, handleUsersChange,
handleModelsChange handleModelsChange,
resetQuery
}; };
} }
+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
};
}
@@ -209,6 +209,10 @@ const options: BackendParameter[] = [
{ {
label: '--max-iter-times', label: '--max-iter-times',
value: '--max-iter-times' value: '--max-iter-times'
},
{
label: '--openai-support',
value: '--openai-support'
} }
]; ];
@@ -217,7 +217,7 @@ const useModelsColumns = ({
) )
} }
]; ];
}, [sortOrder, clusterList, intl, handleSelect]); }, [sortOrder, clusterList, intl, handleSelect, setModelActionList]);
}; };
export default useModelsColumns; export default useModelsColumns;
@@ -1,12 +1,15 @@
import { PageActionType } from '@/config/types'; import { PageActionType } from '@/config/types';
import { createContext, useContext } from 'react'; import { createContext, useContext } from 'react';
import { maasProviderType } from '.'; import { maasProviderType } from '.';
import { RequiredFields } from './types';
interface FormContextProps { interface FormContextProps {
providerType?: maasProviderType; providerType?: maasProviderType;
action: PageActionType; action: PageActionType;
currentData?: any; currentData?: any;
id?: number; id?: number;
providerFields?: RequiredFields[];
getCustomConfig?: () => Record<string, any>;
} }
const FormContext = createContext<FormContextProps>({} as FormContextProps); const FormContext = createContext<FormContextProps>({} as FormContextProps);
+17
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;
@@ -39,3 +40,19 @@ export interface MaasProviderItem {
api_token_count: number; api_token_count: number;
api_tokens: { hash: string }[]; api_tokens: { hash: string }[];
} }
export interface RequiredFields {
type: 'Input' | 'Select' | 'Password';
name: string;
label: {
text: string;
locale?: boolean;
};
required?: boolean;
placeholder?: string;
description?: {
text: string;
locale?: boolean;
};
rules?: any[];
}
+3 -11
View File
@@ -7,8 +7,9 @@ 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';
import { useFormContext } from '../config/form-context'; import { useFormContext } from '../config/form-context';
import { maasProviderOptions, ProviderEnum } from '../config/providers'; import { maasProviderOptions } from '../config/providers';
import { FormData } from '../config/types'; import { FormData } from '../config/types';
import ProviderConfigs from './provider-configs';
const Basic: React.FC<{ const Basic: React.FC<{
onAPIKeyBlur?: (e: any) => void; onAPIKeyBlur?: (e: any) => void;
@@ -77,16 +78,7 @@ const Basic: React.FC<{
})} })}
/> />
</Form.Item> </Form.Item>
{providerType === ProviderEnum.OPENAI && ( <ProviderConfigs />
<Form.Item<FormData> name={['config', 'openaiCustomUrl']}>
<SealInput.Input
placeholder="http://<your-inference-server>/v1"
label={intl.formatMessage({
id: 'providers.form.custombeckendUrl'
})}
/>
</Form.Item>
)}
<Form.Item<FormData> <Form.Item<FormData>
name="api_key" name="api_key"
rules={[ rules={[
+45 -5
View File
@@ -10,9 +10,16 @@ import { json2Yaml, yaml2Json } from '@/pages/backends/config';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Form } from 'antd'; import { Form } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'; import {
forwardRef,
useEffect,
useImperativeHandle,
useMemo,
useRef
} from 'react';
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 useProviderRequiredFields from '../hooks/use-provider-required-fields';
import AdvanceConfig from './advance-config'; import AdvanceConfig from './advance-config';
import Basic from './basic'; import Basic from './basic';
import SupportedModels from './supported-models'; import SupportedModels from './supported-models';
@@ -44,8 +51,10 @@ const requiredFields = {
const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => { const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
const { action, currentData, onFinish } = props; const { action, currentData, onFinish } = props;
const intl = useIntl(); const intl = useIntl();
const providerRequiredFieldsMap = useProviderRequiredFields();
const [form] = Form.useForm(); const [form] = Form.useForm();
const { getScrollElementScrollableHeight } = useWrapperContext(); const { getScrollElementScrollableHeight } = useWrapperContext();
const configType = Form.useWatch(['config', 'type'], form);
const scrollTabsRef = useRef<any>(null); const scrollTabsRef = useRef<any>(null);
const advanceRef = useRef<any>(null); const advanceRef = useRef<any>(null);
const { const {
@@ -80,6 +89,11 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
} }
]; ];
const providerFields = useMemo(() => {
const currentProviderFields = providerRequiredFieldsMap[configType] || [];
return currentProviderFields;
}, [providerRequiredFieldsMap, configType]);
const formatAPIKeys = (values: FormData) => { const formatAPIKeys = (values: FormData) => {
const apiTokens = values.api_tokens?.filter?.( const apiTokens = values.api_tokens?.filter?.(
(item) => item && item.trim() !== '' (item) => item && item.trim() !== ''
@@ -97,12 +111,21 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
); );
}; };
const getCustomConfig = () => {
const customConfig = yaml2Json(advanceRef.current?.getYamlValue() || '');
const config = form.getFieldValue('config') || {};
return {
...config,
...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, ...values.config,
...yaml2Json(advanceRef.current?.getYamlValue() || '') ...yaml2Json(advanceRef.current?.getYamlValue() || '')
}, },
models: _.uniqBy(values.models, 'name'), models: _.uniqBy(values.models, 'name'),
@@ -138,9 +161,20 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
const apiTokensList = _.get(currentData, 'api_tokens', []).map( const apiTokensList = _.get(currentData, 'api_tokens', []).map(
(item: any) => item.hash || '' (item: any) => item.hash || ''
); );
const customConfigYaml = json2Yaml( const currentProvider = _.get(
_.omit(currentData.config, ['type', 'openaiCustomUrl']) || {} providerRequiredFieldsMap,
[currentData.config.type],
[]
); );
const currentRequiredFields = currentProvider.map(
(item: { name: string }) => item.name
);
const customConfigYaml = json2Yaml(
_.omit(currentData.config, ['type', ...currentRequiredFields]) || {}
);
form.setFieldsValue({ form.setFieldsValue({
...currentData, ...currentData,
models: (currentData.models || []).map((item) => ({ models: (currentData.models || []).map((item) => ({
@@ -170,7 +204,13 @@ 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,
providerFields,
getCustomConfig: getCustomConfig
}}
> >
<Form <Form
form={form} form={form}
+8 -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,12 @@ 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
} }
} }
}); });
@@ -0,0 +1,61 @@
import Password from '@/components/seal-form/password';
import SealInput from '@/components/seal-form/seal-input';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import { useFormContext } from '../config/form-context';
import { FormData } from '../config/types';
const ProviderConfigs = () => {
const intl = useIntl();
const form = Form.useFormInstance<FormData>();
const { providerFields } = useFormContext();
const renderLabel = (item: any) => {
return item.label.locale
? intl.formatMessage({ id: item.label.text })
: item.label.text;
};
const renderDescription = (item: any) => {
return item.description
? item.description.locale
? intl.formatMessage({ id: item.description.text })
: item.description.text
: undefined;
};
return (
<>
{providerFields && providerFields.length > 0
? providerFields?.map((item) => {
return (
<Form.Item
name={['config', item.name]}
rules={item.rules}
key={item.name}
>
{item.type === 'Input' && (
<SealInput.Input
required={item.required}
description={renderDescription(item)}
label={renderLabel(item)}
placeholder={item.placeholder}
></SealInput.Input>
)}
{item.type === 'Password' && (
<Password
required={item.required}
label={renderLabel(item)}
description={renderDescription(item)}
placeholder={item.placeholder}
></Password>
)}
</Form.Item>
);
})
: null}
</>
);
};
export default ProviderConfigs;
@@ -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;
[key: string]: any;
}>({ type: '', api_key: '' });
const { id, action, currentData, getCustomConfig } = useFormContext();
const generateCurrentAPIKey = (currentAPIKey: string) => { const generateCurrentAPIKey = (currentAPIKey: string) => {
if ( if (
@@ -36,28 +39,59 @@ const SupportedModels = () => {
return 0; return 0;
}; };
const checkConfigChange = (current: {
type: string;
api_key: string;
[key: string]: any;
}) => {
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 customConfig = getCustomConfig?.();
const currentConfig = {
type: configType,
api_key: currentAPIKey,
...customConfig
};
// 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
} }
} }
}); });
} }
} catch (error) { } catch (error) {
prevAPIKeyRef.current = ''; prevConfigRef.current = {
type: '',
api_key: ''
};
// If validation fails, reset the provider model list to avoid confusion
} }
}; };
@@ -0,0 +1,197 @@
import useAppUtils from '@/hooks/use-app-utils';
import { useIntl } from '@umijs/max';
import { ProviderEnum } from '../config/providers';
import { RequiredFields } from '../config/types';
const useProviderRequiredFields = () => {
const intl = useIntl();
const { getRuleMessage } = useAppUtils();
const providerRequiredFieldsMap: Record<string, RequiredFields[]> = {
[ProviderEnum.OPENAI]: [
{
type: 'Input',
name: 'openaiCustomUrl',
placeholder: 'http://<your-inference-server>/v1',
required: false,
label: {
text: 'providers.form.custombeckendUrl',
locale: true
}
}
],
[ProviderEnum.AZURE]: [
{
type: 'Input',
name: 'azureServiceUrl',
required: true,
label: {
text: 'providers.form.azureServiceUrl',
locale: true
},
rules: [
{
required: true,
message: getRuleMessage('input', 'providers.form.azureServiceUrl')
}
]
}
],
[ProviderEnum.OLLAMA]: [
{
type: 'Input',
name: 'ollamaServerHost',
required: true,
label: {
text: 'providers.form.ollamaServerHost',
locale: true
},
rules: [
{
required: true,
message: getRuleMessage('input', 'providers.form.ollamaServerHost')
}
]
},
{
type: 'Input',
name: 'ollamaServerPort',
required: true,
label: {
text: 'providers.form.ollamaServerPort',
locale: true
},
rules: [
{
required: true,
message: getRuleMessage('input', 'providers.form.ollamaServerPort')
}
]
}
],
[ProviderEnum.HUNYUAN]: [
{
type: 'Input',
name: 'hunyuanAuthId',
required: true,
label: {
text: 'providers.form.hunyuanAuthId',
locale: true
},
rules: [
{
required: true,
message: getRuleMessage('input', 'providers.form.hunyuanAuthId')
}
]
},
{
type: 'Password',
name: 'hunyuanAuthKey',
required: true,
label: {
text: 'providers.form.hunyuanAuthKey',
locale: true
},
rules: [
{
required: true,
message: getRuleMessage('input', 'providers.form.hunyuanAuthKey')
}
]
}
],
[ProviderEnum.CLOUDFLARE]: [
{
type: 'Input',
name: 'cloudflareAccountId',
required: true,
label: {
text: 'providers.form.cloudflareAccountId',
locale: true
},
rules: [
{
required: true,
message: getRuleMessage(
'input',
'providers.form.cloudflareAccountId'
)
}
]
}
],
[ProviderEnum.DEEPL]: [
{
type: 'Input',
name: 'targetLang',
required: true,
label: {
text: 'providers.form.targetLang',
locale: true
},
rules: [
{
required: true,
message: getRuleMessage('input', 'providers.form.targetLang')
}
]
}
],
[ProviderEnum.BEDROCK]: [
{
type: 'Input',
name: 'awsAccessKey',
required: true,
label: {
text: 'AWS Access Key',
locale: false
},
rules: [
{
required: true,
message: getRuleMessage('input', 'AWS Access Key', false)
}
]
},
{
type: 'Password',
name: 'awsSecretKey',
required: true,
label: {
text: 'AWS Secret Key',
locale: false
},
rules: [
{
required: true,
message: getRuleMessage('input', 'AWS Secret Key', false)
}
]
},
{
type: 'Input',
name: 'awsRegion',
placeholder: intl.formatMessage(
{ id: 'common.help.eg' },
{ content: 'us-eest-1' }
),
required: true,
label: {
text: 'providers.form.awsRegion',
locale: true
},
rules: [
{
required: true,
message: getRuleMessage('input', 'providers.form.awsRegion')
}
]
}
]
};
return providerRequiredFieldsMap;
};
export default useProviderRequiredFields;
@@ -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;
}; };
@@ -90,15 +90,8 @@ const RouteItem: React.FC<TargetItemProps> = ({
</Col> </Col>
<Col span={2}> <Col span={2}>
<CellContent> <CellContent>
{data.weight > 0 && (
<AutoTooltip ghost>
{intl.formatMessage({ id: 'routes.form.target.weight' })}:{' '}
{data.weight}
</AutoTooltip>
)}
{data.fallback_status_codes && {data.fallback_status_codes &&
data.fallback_status_codes?.length > 0 && ( data.fallback_status_codes?.length > 0 ? (
<> <>
{data.weight > 0 && ( {data.weight > 0 && (
<span style={{ marginInline: 8 }}>/</span> <span style={{ marginInline: 8 }}>/</span>
@@ -109,6 +102,11 @@ const RouteItem: React.FC<TargetItemProps> = ({
})} })}
</span> </span>
</> </>
) : (
<AutoTooltip ghost>
{intl.formatMessage({ id: 'routes.form.target.weight' })}:{' '}
{data.weight || 0}
</AutoTooltip>
)} )}
</CellContent> </CellContent>
</Col> </Col>
+3 -9
View File
@@ -139,11 +139,12 @@ const TargetsForm = forwardRef((props, ref) => {
}; };
const handleOnWeightChange = (value: any, index: number) => { const handleOnWeightChange = (value: any, index: number) => {
const weight = value || 0;
const targetList = [...targets]; const targetList = [...targets];
if (targetList[index]) { if (targetList[index]) {
targetList[index] = { targetList[index] = {
...targetList[index], ...targetList[index],
weight: value weight: weight
}; };
form.setFieldValue('targets', [...targetList]); form.setFieldValue('targets', [...targetList]);
} }
@@ -151,7 +152,7 @@ const TargetsForm = forwardRef((props, ref) => {
const newDataList = [...dataList]; const newDataList = [...dataList];
newDataList[index] = { newDataList[index] = {
...newDataList[index], ...newDataList[index],
weight: value weight: weight
}; };
form.validateFields(['targets']); form.validateFields(['targets']);
setDataList(newDataList); setDataList(newDataList);
@@ -256,13 +257,6 @@ const TargetsForm = forwardRef((props, ref) => {
{ {
validator(rule, value) { validator(rule, value) {
if (value && value?.length > 0) { if (value && value?.length > 0) {
if (_.some(value, (item: any) => !item.weight)) {
setValidTriggered(true);
return Promise.reject(
getRuleMessage('input', 'routes.form.target.weight')
);
}
if ( if (
_.some( _.some(
dataList, dataList,
+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'
@@ -219,7 +219,7 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
</> </>
</div> </div>
{tokenResult && ( {tokenResult && (
<div style={{ height: 40 }}> <div style={{ minHeight: 40 }}>
<AlertInfo <AlertInfo
type="danger" type="danger"
message={tokenResult?.errorMessage} message={tokenResult?.errorMessage}
@@ -480,7 +480,7 @@ const GroundSTT: React.FC<MessageProps> = forwardRef((props, ref) => {
</div> </div>
)} )}
{tokenResult && ( {tokenResult && (
<div style={{ height: 40 }}> <div style={{ minHeight: 40 }}>
<AlertInfo <AlertInfo
type="danger" type="danger"
message={tokenResult?.errorMessage} message={tokenResult?.errorMessage}
@@ -454,7 +454,7 @@ const GroundTTS: React.FC<MessageProps> = forwardRef((props, ref) => {
</div> </div>
</div> </div>
{tokenResult && ( {tokenResult && (
<div style={{ height: 40 }}> <div style={{ minHeight: 40 }}>
<AlertInfo <AlertInfo
type="danger" type="danger"
message={tokenResult?.errorMessage} message={tokenResult?.errorMessage}
@@ -436,7 +436,7 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
</div> </div>
<div className="ground-left-footer" style={{ padding: 10 }}> <div className="ground-left-footer" style={{ padding: 10 }}>
{tokenResult && ( {tokenResult && (
<div style={{ height: 40 }}> <div style={{ minHeight: 40 }}>
<AlertInfo <AlertInfo
type="danger" type="danger"
message={tokenResult?.errorMessage} message={tokenResult?.errorMessage}
@@ -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;
+1 -1
View File
@@ -343,7 +343,7 @@ export const registerAddWokerCommandMap = {
}; };
export const AddWorkerDockerNotes: Record<string, string[]> = { export const AddWorkerDockerNotes: Record<string, string[]> = {
[GPUDriverMap.NVIDIA]: [], [GPUDriverMap.NVIDIA]: ['clusters.addworker.nvidiaNotes'],
[GPUDriverMap.AMD]: ['clusters.addworker.amdNotes-01'], [GPUDriverMap.AMD]: ['clusters.addworker.amdNotes-01'],
[GPUDriverMap.MOORE_THREADS]: [], [GPUDriverMap.MOORE_THREADS]: [],
[GPUDriverMap.ASCEND]: [], [GPUDriverMap.ASCEND]: [],