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
+46 -28
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;
}
setCopied(true);
} catch {
// Fallback to execCommand method
if (execCopy(text)) {
setCopied(true);
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) {
+5 -6
View File
@@ -80,13 +80,12 @@ const LabelSelector: React.FC<LabelSelectorProps> = ({
if (!clipboardText || clipboardText.indexOf('=') === -1) return;
e.preventDefault();
const lines = clipboardText
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line && line.includes('='));
const lines = _.split(clipboardText, /\r?\n/)
.map((line: string) => line.trim())
.filter((line: string) => line && line.includes('='));
const parsedData = lines.map((line) => {
const [key, value] = line.split('=').map((part) => part.trim());
const parsedData = lines.map((line: string) => {
const [key, value] = line.split(/=(.+)/).map((s) => s.trim());
return { key, value };
});
+11 -9
View File
@@ -83,15 +83,17 @@ const LabelItem: React.FC<LabelItemProps> = ({
open={open}
title={intl.formatMessage({ id: 'resources.table.key.tips' })}
>
<SealInput.Input
disabled={disabled}
checkStatus="success"
label={intl.formatMessage({ id: 'common.input.key' })}
value={label.key}
onChange={handleOnKeyChange}
onBlur={(e: any) => handleKeyOnBlur(e, 'key')}
onPaste={onPaste}
></SealInput.Input>
<span>
<SealInput.Input
disabled={disabled}
checkStatus="success"
label={intl.formatMessage({ id: 'common.input.key' })}
value={label.key}
onChange={handleOnKeyChange}
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;
}
}
}
`;
+4 -5
View File
@@ -92,7 +92,7 @@ export function useQueryData<Detail, Params = any>(option: {
key: string;
delay?: number;
fetchDetail: (params: Params, options?: any) => Promise<Detail>;
getData?: (response: Detail) => any;
getData?: (response: Detail, params?: any) => any;
errorMsg?: string;
}): {
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;
},
@@ -132,7 +132,7 @@ export function useQueryData<Detail, Params = any>(option: {
onSuccess: () => {},
onError: (error) => {
message.error(
error?.message || errorMsg || `Failed to fetch ${key} list`
error?.message || errorMsg || `Failed to fetch ${key} data`
);
setDetailData({} as Detail);
}
@@ -146,8 +146,7 @@ export function useQueryData<Detail, Params = any>(option: {
useEffect(() => {
return () => {
cancel();
axiosTokenRef.current?.cancel();
cancelRequest();
};
}, []);
+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',
+3 -1
View File
@@ -44,5 +44,7 @@ export default {
'backend.filter.source': 'Filter by source',
'backend.add.custom': 'Custom',
'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':
'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キー',
+3 -1
View File
@@ -44,5 +44,7 @@ export default {
'backend.filter.source': 'Filter by source',
'backend.add.custom': 'Custom',
'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':
'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
@@ -44,7 +44,9 @@ export default {
'backend.filter.source': 'Filter by source',
'backend.add.custom': 'Custom',
'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) ==========
@@ -54,4 +56,5 @@ export default {
// 4. 'backend.add.custom': 'Custom',
// 5. 'backend.add.community': 'Community',
// 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 ==========
+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 密钥',
+3 -1
View File
@@ -41,5 +41,7 @@ export default {
'backend.filter.source': '按来源过滤',
'backend.add.custom': '自定义',
'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.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
+6 -4
View File
@@ -18,9 +18,7 @@ const { Text } = Typography;
loader.config({ monaco });
const Container = styled.div<{ $minHeight: string | number }>`
min-height: ${({ $minHeight }) =>
typeof $minHeight === 'number' ? `${$minHeight}px` : $minHeight};
const Container = styled.div`
position: relative;
border: 1px solid var(--ant-color-border);
border-radius: var(--ant-border-radius);
@@ -153,7 +151,11 @@ const YamlEditor: React.FC<ViewerProps> = forwardRef((props, ref) => {
}, [value]);
return (
<Container $minHeight={height}>
<Container
style={{
minHeight: height
}}
>
<EditorInner
ref={editorRef}
header={renderHeader()}
+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,7 +1,8 @@
import AutoTooltip from '@/components/auto-tooltip';
import FullMarkdown from '@/components/markdown-viewer/full-markdown';
import { BulbOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Tag, Typography } from 'antd';
import { Button, Tag } from 'antd';
import _ from 'lodash';
import React, { useEffect } from 'react';
import styled from 'styled-components';
@@ -146,9 +147,7 @@ const BackendDetail: React.FC<{
</span>
</Subtitle>
<Content>
<Typography.Paragraph style={{ whiteSpace: 'pre-line' }}>
{currentData?.description}
</Typography.Paragraph>
<FullMarkdown content={currentData?.description}></FullMarkdown>
</Content>
</Section>
)}
+31 -10
View File
@@ -1,3 +1,4 @@
import AlertBlockInfo from '@/components/alert-info/block';
import IconFont from '@/components/icon-font';
import ModalFooter from '@/components/modal-footer';
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 [yamlContent, setYamlContent] = useState<string>('');
const [formContent, setFormContent] = useState<FormData>({} as FormData);
const alertRef = useRef<HTMLDivElement>(null);
const showVersionCustomSuffix =
currentData?.backend_source === BackendSourceValueMap.BUILTIN ||
@@ -145,11 +147,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
is_built_in:
data.is_built_in &&
data.backend_source === BackendSourceValueMap.BUILTIN,
..._.pick(values.built_in_version_configs?.[key], [
'image_name',
'run_command',
'entrypoint'
])
..._.pick(values.built_in_version_configs?.[key], versionFields)
}));
return {
@@ -196,6 +194,15 @@ const AddModal: React.FC<AddModalProps> = (props) => {
}
}, [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 (
<GSDrawer
title={title}
@@ -248,11 +255,24 @@ const AddModal: React.FC<AddModalProps> = (props) => {
}
}}
footer={
<ModalFooter
onCancel={onClose}
onOk={onOk}
style={ModalFooterStyle}
></ModalFooter>
<>
{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
onCancel={onClose}
onOk={onOk}
style={ModalFooterStyle}
></ModalFooter>
</>
}
>
<Tabs
@@ -277,6 +297,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
label: intl.formatMessage({ id: 'backend.mode.yaml' }),
children: (
<ImportYAML
height={yamlHeight}
actionStatus={{
action: action,
backendSource: backendSource
@@ -19,12 +19,13 @@ interface ImportYAMLProps {
action: PageActionType;
backendSource: string;
};
height?: string | number;
content?: string;
onSubmit?: (content: string) => void;
}
const ImportYAML: React.FC<ImportYAMLProps> = forwardRef(
({ actionStatus, content = '' }, ref) => {
({ actionStatus, content = '', height }, ref) => {
const intl = useIntl();
const editorRef = useRef<any>(null);
const [fileContent, setFileContent] = useState<string>(
@@ -109,7 +110,7 @@ const ImportYAML: React.FC<ImportYAMLProps> = forwardRef(
<YamlEditor
ref={editorRef}
value={fileContent}
height={'calc(100vh - 260px)'}
height={height || 'calc(100vh - 260px)'}
validateMessage={error}
onUpload={(content) => {
setError('');
+5 -2
View File
@@ -192,7 +192,8 @@ export const customBackendFields = [
'health_check_path',
'default_run_command',
'version_configs',
'default_backend_param'
'default_backend_param',
'default_env'
];
/**
@@ -201,7 +202,8 @@ export const customBackendFields = [
export const builtInBackendFields = [
'description',
'version_configs',
'default_backend_param'
'default_backend_param',
'default_env'
];
export const frameworks = [
@@ -278,6 +280,7 @@ export const yamlTemplate = `# ----------------------------------------
# - custom_framework:
# - required
# - choose from: ${Object.values(GPUDriverMap).join(', ')}, cpu
# - env: optional, map of env key and value
backend_name: vllm-custom
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 isCommunity = backendSource === BackendSourceValueMap.COMMUNITY;
return (
<>
@@ -305,7 +306,7 @@ const VersionsForm: React.FC<AddModalProps> = ({
></Form.Item>
)}
</span>
{(fields.length > 1 || isBuiltin) && (
{(fields.length > 1 || isBuiltin || isCommunity) && (
<Button
size="small"
shape="circle"
@@ -1,5 +1,6 @@
import IconFont from '@/components/icon-font';
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import useBodyScroll from '@/hooks/use-body-scroll';
import { ListItem } from '../config/types';
import useCommunityBackend from './use-community-backend';
@@ -30,7 +31,7 @@ const useCreateBackend = () => {
];
const handleEditBackend = (
action: PageAction,
action: PageActionType,
title: string,
row: ListItem
) => {
@@ -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
};
}
+8 -32
View File
@@ -1,46 +1,22 @@
import { useMemoizedFn } from 'ahooks';
import { Spin } from 'antd';
import { omit } from 'lodash';
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import PageBox from '../_components/page-box';
import { queryDashboardData } from './apis';
import DashboardInner from './components/dahboard-inner';
import DashboardContext from './config/dashboard-context';
import { DashboardProps } from './config/types';
import useQueryDashboard from './services/use-query-dashboard';
const Dashboard: React.FC = () => {
const [data, setData] = useState<DashboardProps>({} as DashboardProps);
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);
}
}
);
const { fetchData, loading, data, cancelRequest } = useQueryDashboard();
useEffect(() => {
fetchDashboardData();
fetchData({});
return () => {
cancelRequest();
};
}, []);
return (
<DashboardContext.Provider
value={{ ...data, fetchData: fetchDashboardData }}
>
<DashboardContext.Provider value={{ ...data, fetchData: fetchData }}>
<PageBox>
<Spin spinning={loading} style={{ minHeight: 300 }}>
<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',
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,15 @@
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>;
}
const FormContext = createContext<FormContextProps>({} as FormContextProps);
+17
View File
@@ -12,6 +12,7 @@ export interface FormData {
api_key: string;
proxy_url: string;
proxy_timeout: number;
proxy_enabled?: boolean;
config: {
type: maasProviderType;
openaiCustomUrl?: string;
@@ -39,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={[
+45 -5
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() !== ''
@@ -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 data = {
..._.omit(values, ['api_key']),
api_tokens: formatAPIKeys(values),
config: {
type: values.config.type,
...values.config,
...yaml2Json(advanceRef.current?.getYamlValue() || '')
},
models: _.uniqBy(values.models, 'name'),
@@ -138,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) => ({
@@ -170,7 +204,13 @@ const ProviderForm: React.FC<ProviderFormProps> = forwardRef((props, ref) => {
getScrollElementScrollableHeight={getScrollElementScrollableHeight}
>
<FormContext.Provider
value={{ action, id: currentData?.id, currentData }}
value={{
action,
id: currentData?.id,
currentData,
providerFields,
getCustomConfig: getCustomConfig
}}
>
<Form
form={form}
+8 -3
View File
@@ -54,7 +54,7 @@ const ModelItem: React.FC<ModelItemProps> = ({
const intl = useIntl();
const form = Form.useFormInstance<FormData>();
const { runTestModel, loading: testLoading } = useTestProviderModel();
const { id, action, currentData } = useFormContext();
const { id, action, currentData, getCustomConfig } = useFormContext();
const [openTip, setOpenTip] = React.useState(false);
const generateCurrentAPIKey = (currentAPIKey: string) => {
@@ -75,6 +75,8 @@ const ModelItem: React.FC<ModelItemProps> = ({
};
const handleTestModel = async () => {
const proxyConfigEnabled = form.getFieldValue('proxy_enabled');
const customConfig = getCustomConfig?.();
const res = await runTestModel({
id: generateID(),
data: {
@@ -82,9 +84,12 @@ const ModelItem: React.FC<ModelItemProps> = ({
api_token: generateCurrentAPIKey(
form.getFieldValue('api_key')
) as string,
proxy_url: form.getFieldValue('proxy_url') || undefined,
proxy_url: proxyConfigEnabled
? form.getFieldValue('proxy_url') || null
: null,
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 { PageAction } from '@/config';
import useAppUtils from '@/hooks/use-app-utils';
import { useIntl } from '@umijs/max';
import { Form } from 'antd';
import _ from 'lodash';
import { useRef } from 'react';
import { useFormContext } from '../config/form-context';
import { FormData, ProviderModel } from '../config/types';
@@ -15,9 +15,12 @@ const SupportedModels = () => {
useQueryProviderModels();
const form = Form.useFormInstance<FormData>();
const modelList = Form.useWatch('models', form) || [];
const prevAPIKeyRef = useRef<string>('');
const { getRuleMessage } = useAppUtils();
const { id, action, currentData } = useFormContext();
const prevConfigRef = useRef<{
type: string;
api_key: string;
[key: string]: any;
}>({ type: '', api_key: '' });
const { id, action, currentData, getCustomConfig } = useFormContext();
const generateCurrentAPIKey = (currentAPIKey: string) => {
if (
@@ -36,28 +39,59 @@ const SupportedModels = () => {
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) => {
try {
await form.validateFields(['api_key', ['config', 'type']]);
const proxyConfigEnabled = form.getFieldValue('proxy_enabled');
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
if (open && prevAPIKeyRef.current !== currentAPIKey && currentAPIKey) {
prevAPIKeyRef.current = currentAPIKey;
if (open && checkConfigChange(currentConfig)) {
prevConfigRef.current = {
...currentConfig
};
fetchProviderModels({
id: generateID(),
data: {
api_token: generateCurrentAPIKey(currentAPIKey) as string,
proxy_url: form.getFieldValue('proxy_url') || undefined,
proxy_url: proxyConfigEnabled
? form.getFieldValue('proxy_url') || null
: null,
config: {
type: form.getFieldValue(['config', 'type']) || ''
type: form.getFieldValue(['config', 'type']) || '',
...customConfig
}
}
});
}
} 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(
async (params: {
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 = createAxiosToken();
@@ -83,7 +87,7 @@ export const useTestProviderModel = () => {
id: number;
data: {
api_token: string;
config: { type: string };
config: { type: string; [key: string]: any };
model_name: string;
proxy_url: string;
};
@@ -90,26 +90,24 @@ const RouteItem: React.FC<TargetItemProps> = ({
</Col>
<Col span={2}>
<CellContent>
{data.weight > 0 && (
{data.fallback_status_codes &&
data.fallback_status_codes?.length > 0 ? (
<>
{data.weight > 0 && (
<span style={{ marginInline: 8 }}>/</span>
)}
<span>
{intl.formatMessage({
id: 'routes.table.label.fallback'
})}
</span>
</>
) : (
<AutoTooltip ghost>
{intl.formatMessage({ id: 'routes.form.target.weight' })}:{' '}
{data.weight}
{data.weight || 0}
</AutoTooltip>
)}
{data.fallback_status_codes &&
data.fallback_status_codes?.length > 0 && (
<>
{data.weight > 0 && (
<span style={{ marginInline: 8 }}>/</span>
)}
<span>
{intl.formatMessage({
id: 'routes.table.label.fallback'
})}
</span>
</>
)}
</CellContent>
</Col>
<Col span={3}>
+3 -9
View File
@@ -139,11 +139,12 @@ const TargetsForm = forwardRef((props, ref) => {
};
const handleOnWeightChange = (value: any, index: number) => {
const weight = value || 0;
const targetList = [...targets];
if (targetList[index]) {
targetList[index] = {
...targetList[index],
weight: value
weight: weight
};
form.setFieldValue('targets', [...targetList]);
}
@@ -151,7 +152,7 @@ const TargetsForm = forwardRef((props, ref) => {
const newDataList = [...dataList];
newDataList[index] = {
...newDataList[index],
weight: value
weight: weight
};
form.validateFields(['targets']);
setDataList(newDataList);
@@ -256,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,
+1 -1
View File
@@ -318,7 +318,7 @@ const ModelRoutes: React.FC = () => {
loading={dataSource.loading}
loadend={dataSource.loadend}
dataSource={dataSource.dataList}
image={<IconFont type="icon-extension-outline" />}
image={<IconFont type="icon-captive_portal" />}
filters={_.omit(queryParams, ['sort_by'])}
noFoundText={intl.formatMessage({
id: 'noresult.routes.nofound'
@@ -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}
@@ -53,15 +53,14 @@ export default function useChatCompletion(
}
const deltaReasoningContent =
_.get(chunk, 'choices.0.delta.reasoning_content', '') === null
? ''
: _.get(chunk, 'choices.0.delta.reasoning_content', '');
_.get(chunk, 'choices.0.delta.reasoning_content', '') ||
_.get(chunk, 'choices.0.delta.reasoning', '') ||
'';
const deltaContent =
_.get(chunk, 'choices.0.delta.content', '') === null
? ''
: _.get(chunk, 'choices.0.delta.content', '');
console.log('deltaContent:', deltaContent);
reasonContentRef.current = reasonContentRef.current + deltaReasoningContent;
contentRef.current = contentRef.current + deltaContent;
+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]: [],