Compare commits

...
19 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
51 changed files with 777 additions and 205 deletions
+45 -27
View File
@@ -1,13 +1,7 @@
import { CheckCircleFilled, CopyOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, message, Tooltip } from 'antd';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState
} from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import AutoTooltip from '../auto-tooltip';
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 textarea = document.createElement('textarea');
textarea.value = value;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
const asyncCopy = async (value: string): Promise<boolean> => {
try {
document.execCommand('copy');
await navigator.clipboard.writeText(value);
return true;
} catch {
} catch (error) {
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 {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
} else {
const success = legacyCopy(text);
if (!success) throw new Error('legacy copy failed');
document.addEventListener('copy', onCopy, { capture: true });
document.execCommand('copy');
return copySuccess;
} catch (error) {
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);
} catch {
return;
}
// Both methods failed
throw new Error('Copy failed');
} catch (error) {
message.error(intl.formatMessage({ id: 'common.copy.fail' }) as string);
}
}, [text, intl]);
};
const tipTitle = useMemo(() => {
if (copied) {
@@ -83,6 +83,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
open={open}
title={intl.formatMessage({ id: 'resources.table.key.tips' })}
>
<span>
<SealInput.Input
disabled={disabled}
checkStatus="success"
@@ -92,6 +93,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
onBlur={(e: any) => handleKeyOnBlur(e, 'key')}
onPaste={onPaste}
></SealInput.Input>
</span>
</Tooltip>
)}
</div>
+31 -24
View File
@@ -3,6 +3,7 @@ import { isNotEmptyValue } from '@/utils/index';
import { useIntl } from '@umijs/max';
import type { CascaderAutoProps } from 'antd';
import { Cascader, Empty, Form } from 'antd';
import classNames from 'classnames';
import _, { cloneDeep } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import AutoTooltip from '../auto-tooltip';
@@ -10,28 +11,6 @@ import { SealFormItemProps } from './types';
import Wrapper from './wrapper';
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: {
data: any;
notFoundContent?: React.ReactNode;
@@ -100,7 +79,9 @@ const SealCascader: React.FC<
alwaysFocus = false,
optionNode,
notFoundContent,
size = 'middle',
tagRender,
displayRender,
...rest
} = props;
const intl = useIntl();
@@ -172,10 +153,34 @@ const SealCascader: React.FC<
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 (
<SelectWrapper>
<Wrapper
className="seal-select-wrapper"
className={classNames('seal-select-wrapper', {
'seal-cascader-wrapper-small': size === 'small'
})}
classList={visible ? 'dropdown-visible' : ''}
status={status}
label={label}
@@ -188,6 +193,7 @@ const SealCascader: React.FC<
<Cascader
{...rest}
placeholder={placeholder}
className={size === 'small' ? 'seal-cascader-small' : ''}
suffixIcon={<IconFont type="icon-down"></IconFont>}
optionRender={(data) => (
<OptionNodes
@@ -196,7 +202,8 @@ const SealCascader: React.FC<
optionNode={optionNode}
></OptionNodes>
)}
tagRender={tagRender ?? renderTag}
tagRender={tagRender || renderTag}
displayRender={displayRender}
ref={inputRef}
options={children ? null : _options}
onFocus={handleOnFocus}
+2 -2
View File
@@ -179,8 +179,8 @@ const SimpleSelect: React.FC<SelectProps & { ref?: any; showTags?: boolean }> =
closable={props.closable}
onClose={props.onClose}
style={{
height: 24,
backgroundColor: 'var(--ant-color-fill-tertiary)',
height: 22,
backgroundColor: 'var(--ant-color-fill-secondary)',
fontSize: 'var(--ant-font-size)'
}}
className="flex-center"
@@ -146,6 +146,9 @@ const SelectWrapper = styled.div`
margin-inline-start: 0 !important;
}
}
&.seal-cascader-small {
height: 40px;
}
.ant-select-input {
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;
}
}
}
`;
+1
View File
@@ -2,6 +2,7 @@ export default {
'apikeys.title': 'API Keys',
'apikeys.table.apikeys': 'keys',
'apikeys.button.create': 'Add API Key',
'apikeys.button.edit': 'Edit API Key',
'apikeys.title.save': 'Save API Key',
'apikeys.form.expiretime': 'Expiration',
'apikeys.form.apikey': 'API Key',
+4 -1
View File
@@ -88,5 +88,8 @@ export default {
'benchmark.form.nonLlmModel.tips':
'Benchmarking currently only supports LLM models',
'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':
'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':
'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.model': 'Please select a model',
'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.table.apikeys': 'キー',
'apikeys.button.create': '新しいAPIキーを作成',
'apikeys.button.edit': 'APIキーを編集',
'apikeys.title.save': 'APIキーを保存',
'apikeys.form.expiretime': '有効期限',
'apikeys.form.apikey': 'APIキー',
+4 -1
View File
@@ -88,5 +88,8 @@ export default {
'benchmark.form.nonLlmModel.tips':
'Benchmarking currently only supports LLM models',
'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':
'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':
'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) ==========
@@ -235,5 +237,6 @@ export default {
// 93. 'clusters.create.k8sTips2': 'You can also skip this step and register it later from the cluster list.',
// 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>',
// 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 ==========
+15 -1
View File
@@ -23,5 +23,19 @@ export default {
'providers.form.rules.tokens': 'Please enter a valid API Key',
'providers.form.rules.model': 'Please select a model',
'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.table.apikeys': 'Ключи',
'apikeys.button.create': 'Создать API-ключ',
'apikeys.button.edit': 'Редактировать API-ключ',
'apikeys.title.save': 'Сохранить API-ключ',
'apikeys.form.expiretime': 'Срок действия',
'apikeys.form.apikey': 'API-ключ',
+4 -1
View File
@@ -88,5 +88,8 @@ export default {
'benchmark.form.nonLlmModel.tips':
'Benchmarking currently only supports LLM models',
'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':
'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':
'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) ==========
@@ -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>',
// 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>.`,
// 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.model': 'Please select a model',
'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.table.apikeys': '密钥',
'apikeys.button.create': '添加 API 密钥',
'apikeys.button.edit': '编辑 API 密钥',
'apikeys.title.save': '保存 API 密钥',
'apikeys.form.expiretime': '过期时间',
'apikeys.form.apikey': 'API 密钥',
+4 -1
View File
@@ -87,5 +87,8 @@ export default {
'benchmark.table.export.results': '导出结果',
'benchmark.form.nonLlmModel.tips': '基准测试目前仅支持 LLM 模型',
'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':
'如果 <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':
'平头哥(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.model': '请选择模型',
'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 {
activeKey,
collapseKeys,
setCollapseKeys,
handleActiveChange,
handleOnCollapseChange,
updateActiveKey
+1 -1
View File
@@ -62,7 +62,7 @@ const APIKeys: React.FC = () => {
const handleEditKey = (record: ListItem) => {
setOpenAddModal({
open: true,
title: 'Edit API Key',
title: intl.formatMessage({ id: 'apikeys.button.edit' }),
action: PageAction.EDIT,
currentData: record
});
+1 -1
View File
@@ -256,7 +256,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
}}
footer={
<>
{action === PageAction.CREATE && (
{action === PageAction.CREATE && open && (
<div style={{ marginInline: 24, paddingTop: 8 }} ref={alertRef}>
<AlertBlockInfo
type="warning"
@@ -5,6 +5,7 @@ import { useIntl } from '@umijs/max';
import { Button, Input, Space } from 'antd';
import _ from 'lodash';
import React from 'react';
import { profileOptions } from '../config';
export interface RightActionsProps {
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
@@ -67,12 +68,38 @@ const RightActions: React.FC<RightActionsProps> = ({
allowClear
onChange={handleGPUChange}
></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
allowClear
placeholder={intl.formatMessage({
id: 'benchmark.table.filter.bymodel'
})}
style={{ width: 200 }}
style={{ width: 180 }}
options={modelOptions}
onChange={(value, option) =>
handleQueryChange({
@@ -32,7 +32,7 @@ const PercentileResult: React.FC = () => {
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',
render: (value: number) => round(value, 2)
},
+8
View File
@@ -36,6 +36,7 @@ export const ProfileValueMap = {
ThroughputMedium: 'Throughput',
LongContextStress: 'Long Context',
GenerationHeavy: 'Generation Heavy',
ShareGPT: 'ShareGPT',
Custom: 'Custom'
};
@@ -44,6 +45,7 @@ export const ProfileLabelMap = {
[ProfileValueMap.ThroughputMedium]: 'benchmark.form.profile.throughput',
[ProfileValueMap.LongContextStress]: 'benchmark.form.profile.longContext',
[ProfileValueMap.GenerationHeavy]: 'benchmark.form.profile.heavy',
[ProfileValueMap.ShareGPT]: 'benchmark.form.profile.ShareGPT',
[ProfileValueMap.Custom]: 'benchmark.form.profile.custom'
};
@@ -71,6 +73,12 @@ export const profileOptions = [
tips: 'benchmark.form.profile.heavy.tips',
value: ProfileValueMap.GenerationHeavy,
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],
id: model.id,
isLeaf: false,
ready_replicas: model.ready_replicas,
children: []
}));
@@ -123,15 +124,20 @@ const ModelInstanceForm: React.FC = () => {
}
// 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) {
setModelList(modelOptions);
return;
}
const instanceList = await fetchInstanceList({ id: selectedllmModel.id });
const instanceOptions = instanceList.map((instance: any) =>
renderInstance(instance)
);
if (selectedllmModel) {
selectedllmModel.children = [...instanceOptions] as never[];
}
@@ -143,7 +143,7 @@ const useColumnSettings = (options: {
title: renderTitle(
`${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',
@@ -138,6 +138,16 @@ const SupportedHardware: React.FC<SupportedHardwareProps> = ({
link: 'https://docs.gpustack.ai/latest/installation/requirements/#hygon-dcu',
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' }),
hiddenTitle: true,
@@ -171,17 +181,6 @@ const SupportedHardware: React.FC<SupportedHardwareProps> = ({
link: 'https://docs.gpustack.ai/latest/installation/requirements/#cambricon-mlu',
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' })} `,
hiddenTitle: true,
@@ -24,9 +24,10 @@ const ExportData: React.FC<{
result,
userList,
modelList,
selectedModels,
query,
setQuery,
handleExport,
resetQuery,
handleDateChange,
handleUsersChange,
handleModelsChange
@@ -37,6 +38,21 @@ const ExportData: React.FC<{
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[] = [
{
title: intl.formatMessage({ id: 'resources.table.index' }),
@@ -63,12 +79,9 @@ const ExportData: React.FC<{
{
title: intl.formatMessage({ id: 'dashboard.usage.export.model' }),
dataIndex: 'model_id',
render: (text: string) => {
return (
<AutoTooltip ghost>
{modelList.find((item) => item.value === text)?.label || text}
</AutoTooltip>
);
render: (text: string, record: any) => {
console.log('render model id: ', record, modelList);
return <AutoTooltip ghost>{getModelName(record)}</AutoTooltip>;
}
},
@@ -91,6 +104,7 @@ const ExportData: React.FC<{
width: 150
}
];
const handleSubmit = () => {
const fileName = `usage-data_${query.start_date || ''}_${query.end_date || ''}.xlsx`;
exportJsonToExcel({
@@ -111,13 +125,18 @@ const ExportData: React.FC<{
user_id: (value: string) => {
return userList.find((item) => item.value === value)?.label || value;
},
model_id: (value: string) => {
return modelList.find((item) => item.value === value)?.label || value;
model_id: (value: string, record: any) => {
return getModelName(record);
}
}
});
};
const handleOnCancel = () => {
onCancel?.();
resetQuery();
};
useEffect(() => {
if (open) {
init();
@@ -126,6 +145,7 @@ const ExportData: React.FC<{
start_date: dayjs().subtract(29, 'days').format('YYYY-MM-DD'),
end_date: dayjs().format('YYYY-MM-DD'),
model_ids: [],
provider_model_names: [],
user_ids: []
});
setResult({
@@ -141,7 +161,7 @@ const ExportData: React.FC<{
title={intl.formatMessage({ id: 'dashboard.usage.export' })}
open={open}
centered={false}
onCancel={onCancel}
onCancel={handleOnCancel}
destroyOnHidden={true}
closeIcon={true}
maskClosable={false}
@@ -153,7 +173,7 @@ const ExportData: React.FC<{
footer={
<ModalFooter
onOk={handleSubmit}
onCancel={onCancel}
onCancel={handleOnCancel}
okText={intl.formatMessage({ id: 'common.button.export' })}
></ModalFooter>
}
@@ -164,6 +184,8 @@ const ExportData: React.FC<{
query={query}
userList={userList}
modelList={modelList}
selectedModels={selectedModels}
cascaderWidth={360}
handleDateChange={handleDateChange}
handleUsersChange={handleUsersChange}
handleModelsChange={handleModelsChange}
@@ -171,7 +193,7 @@ const ExportData: React.FC<{
<Table
columns={exportTableColumns}
tableLayout={'auto'}
style={{ width: '100%', marginTop: '16px' }}
style={{ width: '100%', marginTop: '16px', minHeight: 300 }}
dataSource={result.data?.items || []}
loading={loading}
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 ProviderLogo from '@/pages/maas-provider/components/provider-logo';
import { DownloadOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, DatePicker, Tooltip } from 'antd';
@@ -8,12 +11,26 @@ import styled from 'styled-components';
import { DASHBOARD_STATS_API } from '../../apis';
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 = {
maxRange: 60,
defaultRange: 29
};
const FilterWrapper = styled.div`
position: relative;
z-index: 10;
display: flex;
justify-content: space-between;
align-items: center;
@@ -36,8 +53,10 @@ interface FilterBarProps {
handleUsersChange: (value: any) => void;
handleModelsChange: (value: any) => void;
handleExport?: () => void;
selectedModels: string[][];
url: string;
disabledDate?: boolean;
cascaderWidth?: number;
}
const FilterBar: React.FC<FilterBarProps> = (props) => {
@@ -45,6 +64,8 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
query,
userList,
modelList,
selectedModels,
cascaderWidth = 300,
handleDateChange,
handleUsersChange,
handleModelsChange,
@@ -52,7 +73,6 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
url,
disabledDate
} = props;
const { disabledRangeDaysDate, rangePresets } = useRangePickerPreset({
range: DefaultDateConfig.maxRange,
disabledDate: disabledDate
@@ -60,6 +80,50 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
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 (
<FilterWrapper>
<div className="selection">
@@ -89,19 +153,37 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
value={query.user_ids}
onChange={handleUsersChange}
></SimpleSelect>
<SimpleSelect
allowClear
<SealCascader
showSearch
mode="multiple"
options={modelList}
maxTagCount={0}
multiple={true}
onChange={handleModelsChange}
classNames={{
popup: {
root: 'cascader-popup-wrapper gpu-selector'
}
}}
styles={{
root: {
width: cascaderWidth
},
popup: {
listItem: {
padding: '5px 10px'
}
}
}}
maxTagCount={1}
size="small"
placeholder={intl.formatMessage({
id: 'dashboard.usage.selectmodel'
})}
value={query.model_ids}
style={{ maxWidth: 200, minWidth: 160 }}
onChange={handleModelsChange}
></SimpleSelect>
options={modelList}
value={selectedModels}
showCheckedStrategy="SHOW_CHILD"
displayRender={displayRender}
optionNode={optionRender}
getPopupContainer={(triggerNode) => triggerNode.parentNode}
></SealCascader>
{url === DASHBOARD_STATS_API && (
<Tooltip title={intl.formatMessage({ id: 'common.button.export' })}>
<Button icon={<DownloadOutlined />} onClick={handleExport}></Button>
@@ -16,6 +16,7 @@ import useUsageData from './use-usage-data';
const TitleWrapper = styled.div`
margin: 0;
font-weight: 700;
min-width: max-content;
`;
const UsageInner: FC<{ maxWidth: number }> = ({ maxWidth }) => {
@@ -27,6 +28,7 @@ const UsageInner: FC<{ maxWidth: number }> = ({ maxWidth }) => {
query,
userList,
modelList,
selectedModels,
handleOnCancel,
init,
handleExport,
@@ -109,6 +111,7 @@ const UsageInner: FC<{ maxWidth: number }> = ({ maxWidth }) => {
query={query}
userList={userList}
modelList={modelList}
selectedModels={selectedModels}
disabledDate={true}
handleDateChange={handleDateChange}
handleUsersChange={handleUsersChange}
@@ -1,5 +1,4 @@
import { queryModelsList } from '@/pages/llmodels/apis';
import { ListItem as ModelListItem } from '@/pages/llmodels/config/types';
import useTargetSourceModels from '@/pages/model-routes/hooks/use-target-source-models';
import { queryUsersList } from '@/pages/users/apis';
import dayjs from 'dayjs';
import _ from 'lodash';
@@ -93,6 +92,7 @@ export default function useUseageData<T>(config: {
start_date: string;
end_date: string;
model_ids: number[];
provider_model_names: string[];
user_ids: number[];
}>({
start_date: dayjs()
@@ -100,12 +100,14 @@ export default function useUseageData<T>(config: {
.format('YYYY-MM-DD'),
end_date: dayjs().format('YYYY-MM-DD'),
model_ids: [],
user_ids: []
user_ids: [],
provider_model_names: []
});
const [modelList, setModelList] = useState<Global.BaseOption<string>[]>([]);
const { sourceModels: modelList, fetchSourceModels } =
useTargetSourceModels();
const [userList, setUserList] = useState<Global.BaseOption<string>[]>([]);
const [loading, setLoading] = useState(false);
const [selectedModels, setSelectedModels] = useState<string[][]>([]);
const usageData = useMemo<{
requestTokenData: RequestTokenData;
@@ -209,25 +211,6 @@ export default function useUseageData<T>(config: {
};
}, [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 () => {
try {
const params = {
@@ -301,19 +284,49 @@ export default function useUseageData<T>(config: {
});
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) => {
return {
...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 = () => {
fetchUsageData(query);
fetchModelsList();
fetchSourceModels();
fetchUsersList();
};
@@ -325,6 +338,7 @@ export default function useUseageData<T>(config: {
userList,
modelList,
query,
selectedModels,
setQuery,
init,
setResult,
@@ -332,6 +346,7 @@ export default function useUseageData<T>(config: {
handleExport,
handleDateChange,
handleUsersChange,
handleModelsChange
handleModelsChange,
resetQuery
};
}
@@ -209,6 +209,10 @@ const options: BackendParameter[] = [
{
label: '--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;
@@ -1,12 +1,14 @@
import { PageActionType } from '@/config/types';
import { createContext, useContext } from 'react';
import { maasProviderType } from '.';
import { RequiredFields } from './types';
interface FormContextProps {
providerType?: maasProviderType;
action: PageActionType;
currentData?: any;
id?: number;
providerFields?: RequiredFields[];
getCustomConfig?: () => Record<string, any>;
}
+16
View File
@@ -40,3 +40,19 @@ export interface MaasProviderItem {
api_token_count: number;
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 ProviderLogo from '../components/provider-logo';
import { useFormContext } from '../config/form-context';
import { maasProviderOptions, ProviderEnum } from '../config/providers';
import { maasProviderOptions } from '../config/providers';
import { FormData } from '../config/types';
import ProviderConfigs from './provider-configs';
const Basic: React.FC<{
onAPIKeyBlur?: (e: any) => void;
@@ -77,16 +78,7 @@ const Basic: React.FC<{
})}
/>
</Form.Item>
{providerType === ProviderEnum.OPENAI && (
<Form.Item<FormData> name={['config', 'openaiCustomUrl']}>
<SealInput.Input
placeholder="http://<your-inference-server>/v1"
label={intl.formatMessage({
id: 'providers.form.custombeckendUrl'
})}
/>
</Form.Item>
)}
<ProviderConfigs />
<Form.Item<FormData>
name="api_key"
rules={[
+35 -6
View File
@@ -10,9 +10,16 @@ import { json2Yaml, yaml2Json } from '@/pages/backends/config';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
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 { FormData, MaasProviderItem as ListItem } from '../config/types';
import useProviderRequiredFields from '../hooks/use-provider-required-fields';
import AdvanceConfig from './advance-config';
import Basic from './basic';
import SupportedModels from './supported-models';
@@ -44,8 +51,10 @@ const requiredFields = {
const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
const { action, currentData, onFinish } = props;
const intl = useIntl();
const providerRequiredFieldsMap = useProviderRequiredFields();
const [form] = Form.useForm();
const { getScrollElementScrollableHeight } = useWrapperContext();
const configType = Form.useWatch(['config', 'type'], form);
const scrollTabsRef = useRef<any>(null);
const advanceRef = useRef<any>(null);
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 apiTokens = values.api_tokens?.filter?.(
(item) => item && item.trim() !== ''
@@ -99,7 +113,11 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
const getCustomConfig = () => {
const customConfig = yaml2Json(advanceRef.current?.getYamlValue() || '');
return customConfig;
const config = form.getFieldValue('config') || {};
return {
...config,
...customConfig
};
};
const handleOnFinish = (values: FormData) => {
@@ -107,8 +125,7 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
..._.omit(values, ['api_key']),
api_tokens: formatAPIKeys(values),
config: {
type: values.config.type,
openaiCustomUrl: values.config.openaiCustomUrl || undefined,
...values.config,
...yaml2Json(advanceRef.current?.getYamlValue() || '')
},
models: _.uniqBy(values.models, 'name'),
@@ -144,9 +161,20 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
const apiTokensList = _.get(currentData, 'api_tokens', []).map(
(item: any) => item.hash || ''
);
const customConfigYaml = json2Yaml(
_.omit(currentData.config, ['type', 'openaiCustomUrl']) || {}
const currentProvider = _.get(
providerRequiredFieldsMap,
[currentData.config.type],
[]
);
const currentRequiredFields = currentProvider.map(
(item: { name: string }) => item.name
);
const customConfigYaml = json2Yaml(
_.omit(currentData.config, ['type', ...currentRequiredFields]) || {}
);
form.setFieldsValue({
...currentData,
models: (currentData.models || []).map((item) => ({
@@ -180,6 +208,7 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
action,
id: currentData?.id,
currentData,
providerFields,
getCustomConfig: getCustomConfig
}}
>
+1 -5
View File
@@ -89,11 +89,7 @@ const ModelItem: React.FC<ModelItemProps> = ({
: null,
config: {
type: form.getFieldValue(['config', 'type']) || '',
...customConfig,
openaiCustomUrl:
customConfig?.openaiCustomUrl ||
form.getFieldValue(['config', 'openaiCustomUrl']) ||
null
...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;
@@ -18,8 +18,8 @@ const SupportedModels = () => {
const prevConfigRef = useRef<{
type: string;
api_key: string;
openaiCustomUrl: string;
}>({ type: '', api_key: '', openaiCustomUrl: '' });
[key: string]: any;
}>({ type: '', api_key: '' });
const { id, action, currentData, getCustomConfig } = useFormContext();
const generateCurrentAPIKey = (currentAPIKey: string) => {
@@ -42,7 +42,7 @@ const SupportedModels = () => {
const checkConfigChange = (current: {
type: string;
api_key: string;
openaiCustomUrl: string;
[key: string]: any;
}) => {
return (
!_.isEqual(current, prevConfigRef.current) &&
@@ -58,13 +58,12 @@ const SupportedModels = () => {
const proxyConfigEnabled = form.getFieldValue('proxy_enabled');
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 || ''
...customConfig
};
// Avoid repeated requests with the same API key
@@ -82,9 +81,7 @@ const SupportedModels = () => {
: null,
config: {
type: form.getFieldValue(['config', 'type']) || '',
...customConfig,
openaiCustomUrl:
customConfig?.openaiCustomUrl || openaiCustomUrl || null
...customConfig
}
}
});
@@ -92,8 +89,7 @@ const SupportedModels = () => {
} catch (error) {
prevConfigRef.current = {
type: '',
api_key: '',
openaiCustomUrl: ''
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;
@@ -90,15 +90,8 @@ const RouteItem: React.FC<TargetItemProps> = ({
</Col>
<Col span={2}>
<CellContent>
{data.weight > 0 && (
<AutoTooltip ghost>
{intl.formatMessage({ id: 'routes.form.target.weight' })}:{' '}
{data.weight}
</AutoTooltip>
)}
{data.fallback_status_codes &&
data.fallback_status_codes?.length > 0 && (
data.fallback_status_codes?.length > 0 ? (
<>
{data.weight > 0 && (
<span style={{ marginInline: 8 }}>/</span>
@@ -109,6 +102,11 @@ const RouteItem: React.FC<TargetItemProps> = ({
})}
</span>
</>
) : (
<AutoTooltip ghost>
{intl.formatMessage({ id: 'routes.form.target.weight' })}:{' '}
{data.weight || 0}
</AutoTooltip>
)}
</CellContent>
</Col>
-7
View File
@@ -257,13 +257,6 @@ const TargetsForm = forwardRef((props, ref) => {
{
validator(rule, value) {
if (value && value?.length > 0) {
// if (_.some(value, (item: any) => !item.weight)) {
// setValidTriggered(true);
// return Promise.reject(
// getRuleMessage('input', 'routes.form.target.weight')
// );
// }
if (
_.some(
dataList,
@@ -219,7 +219,7 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
</>
</div>
{tokenResult && (
<div style={{ height: 40 }}>
<div style={{ minHeight: 40 }}>
<AlertInfo
type="danger"
message={tokenResult?.errorMessage}
@@ -480,7 +480,7 @@ const GroundSTT: React.FC<MessageProps> = forwardRef((props, ref) => {
</div>
)}
{tokenResult && (
<div style={{ height: 40 }}>
<div style={{ minHeight: 40 }}>
<AlertInfo
type="danger"
message={tokenResult?.errorMessage}
@@ -454,7 +454,7 @@ const GroundTTS: React.FC<MessageProps> = forwardRef((props, ref) => {
</div>
</div>
{tokenResult && (
<div style={{ height: 40 }}>
<div style={{ minHeight: 40 }}>
<AlertInfo
type="danger"
message={tokenResult?.errorMessage}
@@ -436,7 +436,7 @@ const GroundImages: React.FC<MessageProps> = forwardRef((props, ref) => {
</div>
<div className="ground-left-footer" style={{ padding: 10 }}>
{tokenResult && (
<div style={{ height: 40 }}>
<div style={{ minHeight: 40 }}>
<AlertInfo
type="danger"
message={tokenResult?.errorMessage}
+1 -1
View File
@@ -343,7 +343,7 @@ export const registerAddWokerCommandMap = {
};
export const AddWorkerDockerNotes: Record<string, string[]> = {
[GPUDriverMap.NVIDIA]: [],
[GPUDriverMap.NVIDIA]: ['clusters.addworker.nvidiaNotes'],
[GPUDriverMap.AMD]: ['clusters.addworker.amdNotes-01'],
[GPUDriverMap.MOORE_THREADS]: [],
[GPUDriverMap.ASCEND]: [],