feat: modelscope model

This commit is contained in:
jialin
2024-09-22 14:55:23 +08:00
parent b041cdc766
commit 3ab29f9e01
24 changed files with 618 additions and 286 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
import { createFromIconfontCN } from '@ant-design/icons';
const IconFont = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_4613488_7vr8v36d7xp.js'
scriptUrl: '//at.alicdn.com/t/c/font_4613488_4jkdkc8jcf7.js'
});
export default IconFont;
+1
View File
@@ -19,6 +19,7 @@ const KeybindingsMap = {
INPUT: ['Ctrl+K', 'Meta+K'],
NEW1: ['Ctrl+1', 'Meta+1'],
NEW2: ['Ctrl+2', 'Meta+2'],
NEW3: ['Ctrl+3', 'Meta+3'],
FOCUS: ['/', '/'],
ADD: ['Alt+Ctrl+Enter', 'Alt+Meta+Enter']
};
-3
View File
@@ -153,9 +153,6 @@ html {
body * {
font-weight: var(--font-weight-normal);
font-family: 'noto sans', sans-serif;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
}
body {
+1
View File
@@ -176,6 +176,7 @@ export default (props: any) => {
{logo}
<div className="collapse-wrap">
<Button
style={{ marginRight: collapsed ? 0 : -14 }}
size="small"
type={collapsed ? 'default' : 'text'}
onClick={handleToggleCollapse}
+2 -1
View File
@@ -22,7 +22,7 @@ export default {
'model.form.ollama.model': 'Ollama Model',
'model.form.ollamaholder': 'Please select or input model name',
'model.deploy.sort': 'Sort',
'model.deploy.search.placeholder': 'Search models from Hugging Face',
'model.deploy.search.placeholder': 'Search models from {source}',
'model.form.ollamatips':
'Tip: The following are the preconfigured Ollama models in GPUStack. Please select the model you want, or directly enter the model you wish to deploy in the 【{name}】 input box on the right.',
'models.sort.name': 'Name',
@@ -35,6 +35,7 @@ export default {
'models.data.card': 'Model Card',
'models.available.files': 'Available Files',
'models.viewin.hf': 'View in Hugging Face',
'models.viewin.modelscope': 'View in ModelScope',
'models.architecture': 'Architecture',
'models.search.noresult': 'No related models found',
'models.search.nofiles': 'No available files',
+2 -1
View File
@@ -22,7 +22,7 @@ export default {
'model.form.ollama.model': 'Ollama 模型',
'model.form.ollamaholder': '请选择或输入模型名称',
'model.deploy.sort': '排序',
'model.deploy.search.placeholder': '从 Hugging Face 搜索模型',
'model.deploy.search.placeholder': '从 {source} 搜索模型',
'model.form.ollamatips':
'提示:以下为 GPUStack 预设的 Ollama 模型,请选择你想要的模型或者直接在右侧表单 【{name}】 输入框中输入你要部署的模型。',
'models.sort.name': '名称',
@@ -35,6 +35,7 @@ export default {
'models.data.card': '模型简介',
'models.available.files': '可用文件',
'models.viewin.hf': '在 Hugging Face 中查看',
'models.viewin.modelscope': '在 ModelScope 中查看',
'models.architecture': '架构',
'models.search.noresult': '未找到相关模型',
'models.search.nofiles': '无可用文件',
+72 -3
View File
@@ -1,6 +1,7 @@
import { downloadFile, listFiles, listModels } from '@huggingface/hub';
import { PipelineType } from '@huggingface/tasks';
import { request } from '@umijs/max';
import qs from 'query-string';
import {
FormData,
GPUListItem,
@@ -126,6 +127,15 @@ export async function callHuggingfaceQuickSearch(params: any) {
const HUGGINGFACE_API = 'https://huggingface.co/api/models';
const MODEL_SCOPE_LIST_MODEL_API =
'/proxy?url=https://www.modelscope.cn/api/v1/dolphin/models';
const MODEL_SCOPE_DETAIL_MODEL_API =
'/proxy?url=https://www.modelscope.cn/api/v1/dolphin/models/';
const MODE_SCOPE_MODEL_FIELS_API =
'/proxy?url=https://modelscope.cn/api/v1/models/';
export async function queryHuggingfaceModelDetail(
params: { repo: string },
options?: any
@@ -136,10 +146,69 @@ export async function queryHuggingfaceModelDetail(
});
}
export async function queryModelScopeModels() {
return request(`https://www.modelscope.cn/api/v1/dolphin/models`, {
method: 'PUT'
export async function queryModelScopeModels(
params: {
PageSize?: number;
PageNumber?: number;
SortBy?: string;
Target?: string;
SingleCriterion?: any[];
Name: string;
},
config?: any
) {
const res = await fetch(`${MODEL_SCOPE_LIST_MODEL_API}`, {
method: 'PUT',
signal: config?.signal,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
...params,
Name: `${params.Name} gguf`,
PageSize: 100,
PageNumber: 1
})
});
if (!res.ok) {
throw new Error('Network response was not ok');
return null;
}
return res.json();
}
export async function queryModelScopeModelDetail(
params: { name: string },
options?: any
) {
return request(`${MODE_SCOPE_MODEL_FIELS_API}${params.name}`, {
method: 'GET',
cancelToken: options?.token
});
}
export async function queryModelScopeModelFiles(
params: { name: string },
options?: any
) {
const res = await fetch(
`${MODE_SCOPE_MODEL_FIELS_API}${params.name}/repo/files?${qs.stringify({
Revision: 'master',
Root: ''
})}`,
{
method: 'GET',
signal: options?.signal,
body: null
}
);
if (!res.ok) {
throw new Error('Network response was not ok');
return null;
}
return res.json();
}
export async function queryHuggingfaceModels(
+37 -24
View File
@@ -10,6 +10,7 @@ import React, {
forwardRef,
useEffect,
useImperativeHandle,
useMemo,
useState
} from 'react';
import { queryGPUList } from '../apis';
@@ -21,7 +22,7 @@ interface DataFormProps {
ref?: any;
source: string;
action: PageActionType;
repo: string;
selectedModel: any;
onOk: (values: FormData) => void;
}
@@ -35,11 +36,20 @@ const sourceOptions = [
label: 'Ollama Library',
value: modelSourceMap.ollama_library_value,
key: 'ollama_library'
},
{
label: 'ModelScope',
value: modelSourceMap.modelscope_value,
key: 'model_scope'
}
];
const SEARCH_SOURCE = [
modelSourceMap.huggingface_value,
modelSourceMap.modelscope_value
];
const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
const { action, repo, onOk } = props;
const { action, onOk } = props;
const [form] = Form.useForm();
const intl = useIntl();
const [gpuOptions, setGpuOptions] = useState<
@@ -86,22 +96,21 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
);
const handleOnSelectModel = () => {
console.log('repo=============', repo);
if (!repo) {
if (!props.selectedModel.name) {
return;
}
let name = _.split(repo, '/').slice(-1)[0];
let name = _.split(props.selectedModel.name, '/').slice(-1)[0];
const reg = /(-gguf)$/i;
name = _.toLower(name).replace(reg, '');
if (props.source === modelSourceMap.huggingface_value) {
if (SEARCH_SOURCE.includes(props.source)) {
form.setFieldsValue({
huggingface_repo_id: repo,
repo_id: props.selectedModel.name,
name: name
});
} else {
form.setFieldsValue({
ollama_library_model_name: repo,
ollama_library_model_name: props.selectedModel.name,
name: name
});
}
@@ -111,8 +120,8 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
return (
<>
<Form.Item<FormData>
name="huggingface_repo_id"
key="huggingface_repo_id"
name="repo_id"
key="repo_id"
rules={[
{
required: true,
@@ -132,7 +141,8 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
></SealInput.Input>
</Form.Item>
<Form.Item<FormData>
name="huggingface_filename"
name="file_name"
key="file_name"
rules={[
{
required: true,
@@ -214,18 +224,21 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
);
};
const renderFieldsBySource = () => {
switch (props.source) {
case modelSourceMap.huggingface_value:
return renderHuggingfaceFields();
case modelSourceMap.ollama_library_value:
return renderOllamaModelFields();
case modelSourceMap.s3_value:
return renderS3Fields();
default:
return null;
const renderFieldsBySource = useMemo(() => {
if (SEARCH_SOURCE.includes(props.source)) {
return renderHuggingfaceFields();
}
};
if (props.source === modelSourceMap.ollama_library_value) {
return renderOllamaModelFields();
}
if (props.source === modelSourceMap.s3_value) {
return renderS3Fields();
}
return null;
}, [props.source]);
const handleOk = (formdata: FormData) => {
const gpu = _.find(gpuOptions, (item: any) => {
@@ -249,7 +262,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
useEffect(() => {
handleOnSelectModel();
}, [repo]);
}, [props.selectedModel.name]);
return (
<Form
@@ -314,7 +327,7 @@ const DataForm: React.FC<DataFormProps> = forwardRef((props, ref) => {
></SealSelect>
}
</Form.Item>
{renderFieldsBySource()}
{renderFieldsBySource}
<Form.Item<FormData>
name="replicas"
rules={[
+41 -35
View File
@@ -36,18 +36,22 @@ const AddModal: React.FC<AddModalProps> = (props) => {
action,
width = 600
} = props || {};
const SEARCH_SOURCE = [
modelSourceMap.huggingface_value,
modelSourceMap.modelscope_value
];
const form = useRef<any>({});
const intl = useIntl();
const [huggingfaceRepoId, setHuggingfaceRepoId] = useState<string>('');
const [selectedModel, setSelectedModel] = useState<any>({});
const [collapsed, setCollapsed] = useState<boolean>(false);
const [loadingModel, setLoadingModel] = useState<boolean>(false);
const handleSelectModelFile = useCallback((item: any) => {
form.current?.setFieldValue?.('huggingface_filename', item.fakeName);
form.current?.setFieldValue?.('file_name', item.fakeName);
}, []);
const handleOnSelectModel = (item: any) => {
setHuggingfaceRepoId(item.name);
setSelectedModel(item);
};
const handleSumit = () => {
@@ -56,7 +60,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
useEffect(() => {
return () => {
setHuggingfaceRepoId('');
setSelectedModel({});
};
}, [open]);
@@ -91,41 +95,43 @@ const AddModal: React.FC<AddModalProps> = (props) => {
overflowX: 'hidden'
},
content: {
borderRadius: '8px 0 0 8px'
borderRadius: '6px 0 0 6px'
}
}}
width={width}
footer={false}
>
<div style={{ display: 'flex' }}>
{props.source === modelSourceMap.huggingface_value && (
<div style={{ display: 'flex', flex: 1 }}>
<ColumnWrapper>
<SearchModel
modelSource={props.source}
onSelectModel={handleOnSelectModel}
setLoadingModel={setLoadingModel}
></SearchModel>
</ColumnWrapper>
<Separator></Separator>
</div>
)}
{props.source === modelSourceMap.huggingface_value && (
<div style={{ display: 'flex', flex: 1 }}>
<ColumnWrapper>
<ModelCard
repo={huggingfaceRepoId}
onCollapse={setCollapsed}
collapsed={collapsed}
></ModelCard>
<HFModelFile
repo={huggingfaceRepoId}
onSelectFile={handleSelectModelFile}
collapsed={collapsed}
></HFModelFile>
</ColumnWrapper>
<Separator></Separator>
</div>
{SEARCH_SOURCE.includes(props.source) && (
<>
<div style={{ display: 'flex', flex: 1 }}>
<ColumnWrapper>
<SearchModel
modelSource={props.source}
onSelectModel={handleOnSelectModel}
setLoadingModel={setLoadingModel}
></SearchModel>
</ColumnWrapper>
<Separator></Separator>
</div>
<div style={{ display: 'flex', flex: 1 }}>
<ColumnWrapper>
<ModelCard
selectedModel={selectedModel}
onCollapse={setCollapsed}
collapsed={collapsed}
modelSource={props.source}
></ModelCard>
<HFModelFile
selectedModel={selectedModel}
modelSource={props.source}
onSelectFile={handleSelectModelFile}
collapsed={collapsed}
></HFModelFile>
</ColumnWrapper>
<Separator></Separator>
</div>
</>
)}
<ColumnWrapper
footer={
@@ -141,7 +147,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
}
>
<>
{source === modelSourceMap.huggingface_value && (
{SEARCH_SOURCE.includes(source) && (
<TitleWrapper>
{intl.formatMessage({ id: 'models.form.configurations' })}
<span style={{ display: 'flex', height: 24 }}></span>
@@ -150,7 +156,7 @@ const AddModal: React.FC<AddModalProps> = (props) => {
<DataForm
source={source}
action={action}
repo={huggingfaceRepoId}
selectedModel={selectedModel}
onOk={onOk}
ref={form}
></DataForm>
+58 -15
View File
@@ -7,23 +7,25 @@ import _ from 'lodash';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import SimpleBar from 'simplebar-react';
import 'simplebar-react/dist/simplebar.min.css';
import { queryHuggingfaceModelFiles } from '../apis';
import { queryHuggingfaceModelFiles, queryModelScopeModelFiles } from '../apis';
import { modelSourceMap } from '../config';
import { getFileType } from '../config/file-type';
import '../style/hf-model-file.less';
import FileParts from './file-parts';
import TitleWrapper from './title-wrapper';
interface HFModelFileProps {
repo: string;
selectedModel: any;
collapsed?: boolean;
loadingModel?: boolean;
modelSource: string;
onSelectFile?: (file: any) => void;
}
const pattern = /^(.*)-(\d+)-of-(\d+)\.gguf$/;
const HFModelFile: React.FC<HFModelFileProps> = (props) => {
const { collapsed, loadingModel } = props;
const { collapsed, modelSource } = props;
const intl = useIntl();
const [dataSource, setDataSource] = useState<any>({
fileList: [],
@@ -109,19 +111,11 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
return [...shardFileListResult, ...newGeneralFileList];
}, []);
const handleFetchModelFiles = async () => {
if (!props.repo) {
setDataSource({ fileList: [], loading: false });
handleSelectModelFile({});
return;
}
axiosTokenRef.current?.abort?.();
axiosTokenRef.current = new AbortController();
setDataSource({ ...dataSource, loading: true });
setCurrent('');
// hugging face files
const getHuggingfaceFiles = async () => {
try {
const res = await queryHuggingfaceModelFiles(
{ repo: props.repo },
{ repo: props.selectedModel.name || '' },
{
signal: axiosTokenRef.current.signal
}
@@ -133,6 +127,55 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
const list = _.filter(fileList, (file: any) => {
return _.endsWith(file.path, '.gguf') || _.includes(file.path, '.gguf');
});
return list;
} catch (error) {
return [];
}
};
// modelscope files
const getModelScopeFiles = async () => {
try {
const data = await queryModelScopeModelFiles(
{
name: props.selectedModel.name || ''
},
{
signal: axiosTokenRef.current.signal
}
);
const fileList = _.filter(_.get(data, ['Data', 'Files']), (file: any) => {
return _.endsWith(file.Path, '.gguf') || _.includes(file.Path, '.gguf');
});
const list = _.map(fileList, (item: any) => {
return {
path: item.Path,
size: item.Size
};
});
return list;
} catch (error) {
return [];
}
};
const handleFetchModelFiles = async () => {
if (!props.selectedModel.name) {
setDataSource({ fileList: [], loading: false });
handleSelectModelFile({});
return;
}
axiosTokenRef.current?.abort?.();
axiosTokenRef.current = new AbortController();
setDataSource({ ...dataSource, loading: true });
setCurrent('');
try {
let list = [];
if (modelSourceMap.huggingface_value === modelSource) {
list = await getHuggingfaceFiles();
} else if (modelSourceMap.modelscope_value === modelSource) {
list = await getModelScopeFiles();
}
const newList = generateGroupByFilename(list);
const sortList = _.sortBy(newList, (item: any) => {
@@ -186,7 +229,7 @@ const HFModelFile: React.FC<HFModelFileProps> = (props) => {
useEffect(() => {
handleFetchModelFiles();
}, [props.repo]);
}, [props.selectedModel.name]);
useEffect(() => {
return () => {
@@ -25,6 +25,11 @@ interface HFModelItemProps {
}
const warningTask = ['image', 'audio', 'video'];
const SUPPORTEDSOURCE = [
modelSourceMap.huggingface_value,
modelSourceMap.modelscope_value
];
const HFModelItem: React.FC<HFModelItemProps> = (props) => {
const intl = useIntl();
const isExcludeTask = () => {
@@ -57,7 +62,7 @@ const HFModelItem: React.FC<HFModelItemProps> = (props) => {
)}
</div>
<div className="info">
{props.source === modelSourceMap.huggingface_value ? (
{SUPPORTEDSOURCE.includes(props.source || '') ? (
<div className="info-item">
{/* {props.task && (
<Tag
+90 -26
View File
@@ -11,17 +11,23 @@ import { Button, Empty, Tag, Tooltip } from 'antd';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import SimpleBar from 'simplebar-react';
import 'simplebar-react/dist/simplebar.min.css';
import { downloadModelFile, queryHuggingfaceModelDetail } from '../apis';
import {
downloadModelFile,
queryHuggingfaceModelDetail,
queryModelScopeModelDetail
} from '../apis';
import { modelSourceMap } from '../config';
import '../style/model-card.less';
import TitleWrapper from './title-wrapper';
const ModelCard: React.FC<{
repo: string;
selectedModel: any;
onCollapse: (flag: boolean) => void;
collapsed: boolean;
loadingModel?: boolean;
modelSource: string;
}> = (props) => {
const { repo, onCollapse, collapsed, loadingModel } = props;
const { onCollapse, collapsed, modelSource } = props;
const intl = useIntl();
const requestSource = useRequestToken();
const [modelData, setModelData] = useState<any>({});
@@ -49,28 +55,60 @@ const ModelCard: React.FC<{
}
};
const getModelCardData = async () => {
if (!repo) {
setModelData(null);
return;
}
requestToken.current?.cancel?.();
requestToken.current = requestSource();
// huggingface model card data
const getHuggingfaceModelDetail = async () => {
try {
const [modelcard, readme] = await Promise.all([
queryHuggingfaceModelDetail(
{ repo },
{ repo: props.selectedModel.name },
{
token: requestToken.current.token
}
),
loadFile(repo, 'main')
loadFile(props.selectedModel.name, 'main')
]);
setModelData(modelcard);
setReadmeText(readme);
} catch (error) {
setModelData({});
setReadmeText(null);
}
};
const getModelScopeModelDetail = async () => {
try {
const data = await queryModelScopeModelDetail(
{
name: props.selectedModel.name
},
{
token: requestToken.current.token
}
);
console.log('detaildata==========', data);
setModelData({
...data?.Data,
name: data?.Data?.Name
});
setReadmeText(data?.Data?.ReadMeContent);
} catch (error) {
setModelData({});
setReadmeText(null);
}
};
const getModelCardData = async () => {
if (!props.selectedModel.name) {
setModelData(null);
return;
}
requestToken.current?.cancel?.();
requestToken.current = requestSource();
if (modelSource === modelSourceMap.huggingface_value) {
getHuggingfaceModelDetail();
} else if (modelSource === modelSourceMap.modelscope_value) {
getModelScopeModelDetail();
}
};
@@ -78,9 +116,46 @@ const ModelCard: React.FC<{
onCollapse(!collapsed);
}, [collapsed]);
const generateModelLink = () => {
const name = modelData?.id || modelData?.name;
if (!name) {
return null;
}
if (modelSource === modelSourceMap.huggingface_value) {
return (
<Tooltip title={intl.formatMessage({ id: 'models.viewin.hf' })}>
<Button
size="small"
type="link"
target="_blank"
href={`https://huggingface.co/${modelData.id}`}
>
<IconFont type="icon-external-link"></IconFont>
</Button>
</Tooltip>
);
}
if (modelSource === modelSourceMap.modelscope_value) {
return (
<Tooltip title={intl.formatMessage({ id: 'models.viewin.modelscope' })}>
<Button
size="small"
type="link"
target="_blank"
href={`https://modelscope.cn/models/${modelData?.Path}/${modelData.name}`}
>
<IconFont type="icon-external-link"></IconFont>
</Button>
</Tooltip>
);
}
return null;
};
useEffect(() => {
getModelCardData();
}, [repo]);
}, [props.selectedModel.name]);
useEffect(() => {
if (!readmeText) {
@@ -98,19 +173,8 @@ const ModelCard: React.FC<{
return (
<>
<TitleWrapper>
<div className="title">{modelData?.id} </div>
{modelData?.id && (
<Tooltip title={intl.formatMessage({ id: 'models.viewin.hf' })}>
<Button
size="small"
type="link"
target="_blank"
href={`https://huggingface.co/${modelData.id}`}
>
<IconFont type="icon-external-link"></IconFont>
</Button>
</Tooltip>
)}
<div className="title">{modelData?.id || modelData?.name} </div>
{generateModelLink()}
</TitleWrapper>
<div className="card-wrapper">
{modelData ? (
+23 -13
View File
@@ -1,41 +1,51 @@
import IconFont from '@/components/icon-font';
import hotkeys from '@/config/hotkeys';
import { SearchOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Input } from 'antd';
import React, { useRef, useState } from 'react';
import React, { useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { modelSourceMap, modelSourceValueMap } from '../config';
const SearchInput: React.FC<{
modelSource: string;
onSearch: (e: any) => void;
}> = (props) => {
const { onSearch } = props;
const { onSearch, modelSource } = props;
const intl = useIntl();
const [isFocus, setIsFocus] = useState(false);
const inputRef = useRef<any>(null);
useHotkeys(hotkeys.INPUT.join(','), () => {
useHotkeys(hotkeys.FOCUS, (e: any) => {
e.preventDefault();
inputRef.current?.focus?.();
setIsFocus(true);
});
return (
<Input
ref={inputRef}
onPressEnter={onSearch}
onFocus={() => setIsFocus(true)}
onBlur={() => setIsFocus(false)}
allowClear
placeholder={intl.formatMessage({
id: 'model.deploy.search.placeholder'
})}
placeholder={intl.formatMessage(
{
id: 'model.deploy.search.placeholder'
},
{ source: modelSourceValueMap[modelSource] }
)}
prefix={
<>
<SearchOutlined
{/* <SearchOutlined
style={{
fontSize: '16px',
color: 'var(--ant-color-text-quaternary)'
}}
/>
/> */}
<IconFont
className="font-size-16"
type={
modelSource === modelSourceMap.huggingface_value
? 'icon-huggingface'
: 'icon-tu2'
}
></IconFont>
</>
}
></Input>
+84 -91
View File
@@ -1,10 +1,15 @@
import { BulbOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Input, Select } from 'antd';
import { Select } from 'antd';
import _ from 'lodash';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { queryHuggingfaceModels } from '../apis';
import { ModelSortType, modelSourceMap, ollamaModelOptions } from '../config';
import { queryHuggingfaceModels, queryModelScopeModels } from '../apis';
import {
ModelScopeSortType,
ModelSortType,
modelSourceMap,
ollamaModelOptions
} from '../config';
import SearchStyle from '../style/search-result.less';
import SearchInput from './search-input';
import SearchResult from './search-result';
@@ -31,10 +36,13 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
networkError: false,
sortType: ModelSortType.trendingScore
});
const SUPPORTEDSOURCE = [
modelSourceMap.huggingface_value,
modelSourceMap.modelscope_value
];
const [current, setCurrent] = useState<string>('');
const cacheRepoOptions = useRef<any[]>([]);
const axiosTokenRef = useRef<any>(null);
const customOllamaModelRef = useRef<any>(null);
const searchInputRef = useRef<any>('');
const modelFilesSortOptions = useRef<any[]>([
{
@@ -56,16 +64,74 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
]);
const handleOnSelectModel = useCallback((item: any) => {
console.log('handleOnSelectModel', item);
onSelectModel(item);
setCurrent(item.id);
}, []);
// huggeface
const getModelsFromHuggingface = useCallback(async (sort: string) => {
try {
const task: any = searchInputRef.current ? '' : 'text-generation';
const params = {
search: {
query: searchInputRef.current || '',
sort: sort,
tags: ['gguf'],
task
}
};
const data = await queryHuggingfaceModels(params, {
signal: axiosTokenRef.current.signal
});
let list = _.map(data || [], (item: any) => {
return {
...item,
value: item.name,
label: item.name
};
});
return list;
} catch (error) {
return [];
}
}, []);
// modelscope
const getModelsFromModelscope = useCallback(async (sort: string) => {
try {
const params = {
Name: searchInputRef.current || '',
SortBy: ModelScopeSortType[sort]
};
const data = await queryModelScopeModels(params, {
signal: axiosTokenRef.current.signal
});
let list = _.map(_.get(data, 'Data.Model.Models') || [], (item: any) => {
return {
path: item.Path,
name: `${item.Path}/${item.Name}`,
downloads: item.Downloads,
id: item.Name,
updatedAt: item.LastUpdatedTime * 1000,
likes: item.Stars,
value: item.Name,
label: item.Name,
task: item.Tasks?.map((sItem: any) => sItem.Name).join(',')
};
});
return list;
} catch (error) {
return [];
}
}, []);
const handleOnSearchRepo = useCallback(
async (sortType?: string) => {
if (modelSource === modelSourceMap.ollama_library_value) {
if (!SUPPORTEDSOURCE.includes(modelSource)) {
return;
}
console.log('handleOnSearchRepo', dataSource.loading);
axiosTokenRef.current?.abort?.();
axiosTokenRef.current = new AbortController();
if (dataSource.loading) return;
@@ -77,26 +143,12 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
});
setLoadingModel?.(true);
cacheRepoOptions.current = [];
const task: any = searchInputRef.current ? '' : 'text-generation';
const params = {
search: {
query: searchInputRef.current || '',
sort: sort,
tags: ['gguf'],
task
}
};
const models = await queryHuggingfaceModels(params, {
signal: axiosTokenRef.current.signal
});
let list = _.map(models || [], (item: any) => {
return {
...item,
value: item.name,
label: item.name
};
});
let list: any[] = [];
if (modelSource === modelSourceMap.huggingface_value) {
list = await getModelsFromHuggingface(sort);
} else if (modelSource === modelSourceMap.modelscope_value) {
list = await getModelsFromModelscope(sort);
}
cacheRepoOptions.current = list;
setDataSource({
repoOptions: list,
@@ -133,7 +185,7 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
if (
!dataSource.repoOptions.length &&
!cacheRepoOptions.current.length &&
modelSource === modelSourceMap.huggingface_value
SUPPORTEDSOURCE.includes(modelSource)
) {
handleOnSearchRepo();
}
@@ -149,51 +201,6 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
}
};
const handleFilterModels = (e: any) => {
const text = e.target.value;
const list = _.filter(cacheRepoOptions.current, (item: any) => {
return item.name.includes(text);
});
setDataSource({
repoOptions: list,
loading: false,
networkError: false,
sortType: dataSource.sortType
});
};
const debounceFilter = _.debounce((e: any) => {
handleFilterModels(e);
}, 300);
const handleSourceChange = (source: string) => {
axiosTokenRef.current?.abort?.();
onSourceChange?.(source);
setDataSource({
repoOptions: [],
loading: false,
networkError: false,
sortType: dataSource.sortType
});
cacheRepoOptions.current = [];
};
const handleInputChange = (e: any) => {
const value = e.target.value;
customOllamaModelRef.current = value;
};
const handleConfirm = () => {
const model = {
label: customOllamaModelRef.current,
value: customOllamaModelRef.current,
name: customOllamaModelRef.current,
id: ''
};
onSelectModel(model);
setCurrent('');
};
const handleSortChange = (value: string) => {
handleOnSearchRepo(value || '');
};
@@ -201,7 +208,10 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
const renderHFSearch = () => {
return (
<>
<SearchInput onSearch={handlerSearchModels}></SearchInput>
<SearchInput
onSearch={handlerSearchModels}
modelSource={modelSource}
></SearchInput>
<div className={SearchStyle.filter}>
<span>
<span className="value">
@@ -231,23 +241,6 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
);
};
const renderOllamaCustom = () => {
return (
<>
<Input
allowClear
placeholder="Input ollama model name"
onChange={handleInputChange}
></Input>
<div className={SearchStyle.filter}>
<Button type="primary" onClick={handleConfirm}>
{intl.formatMessage({ id: 'common.button.confirm' })}
</Button>
</div>
</>
);
};
useEffect(() => {
handleOnOpen();
console.log('SearchModel useEffect', modelSource);
@@ -262,7 +255,7 @@ const SearchModel: React.FC<SearchInputProps> = (props) => {
return (
<div style={{ flex: 1 }}>
<div className={SearchStyle['search-bar']}>
{modelSource === modelSourceMap.huggingface_value ? (
{SUPPORTEDSOURCE.includes(modelSource) ? (
renderHFSearch()
) : (
<div style={{ lineHeight: '18px' }}>
@@ -1,7 +1,7 @@
import IconFont from '@/components/icon-font';
import { SearchOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Col, Empty, Row, Spin } from 'antd';
import { Col, Empty, Row, Spin } from 'antd';
import React from 'react';
import SimpleBar from 'simplebar-react';
import 'simplebar-react/dist/simplebar.min.css';
@@ -53,7 +53,7 @@ const SearchResult: React.FC<SearchResultProps> = (props) => {
<span>
{intl.formatMessage({ id: 'models.search.networkerror' })}
</span>
<span>
{/* <span>
<span>
{intl.formatMessage({ id: 'models.search.hfvisit' })}
</span>
@@ -65,7 +65,7 @@ const SearchResult: React.FC<SearchResultProps> = (props) => {
>
Hugging Face
</Button>
</span>
</span> */}
</div>
}
/>
+53 -11
View File
@@ -38,7 +38,7 @@ import {
queryModelInstancesList,
updateModel
} from '../apis';
import { modelSourceMap } from '../config';
import { getSourceRepoConfigValue, modelSourceMap } from '../config';
import { FormData, ListItem, ModelInstanceListItem } from '../config/types';
import DeployModal from './deploy-modal';
import InstanceItem from './instance-item';
@@ -95,9 +95,7 @@ const Models: React.FC<ModelsProps> = ({
source: modelSourceMap.huggingface_value
});
const [title, setTitle] = useState<string>('');
const [currentData, setCurrentData] = useState<ListItem | undefined>(
undefined
);
const [currentData, setCurrentData] = useState<ListItem>({} as ListItem);
const [currentInstanceUrl, setCurrentInstanceUrl] = useState<string>('');
const modalRef = useRef<any>(null);
@@ -116,6 +114,21 @@ const Models: React.FC<ModelsProps> = ({
}
);
useHotkeys(
HotKeys.NEW3.join(','),
() => {
setOpenDeployModal({
show: true,
width: 'calc(100vw - 220px)',
source: modelSourceMap.modelscope_value
});
},
{
preventDefault: true,
enabled: !openAddModal && !openDeployModal.show && !openLogModal
}
);
useHotkeys(
HotKeys.NEW2.join(','),
() => {
@@ -159,6 +172,19 @@ const Models: React.FC<ModelsProps> = ({
};
});
}
},
{
label: 'ModelScope',
value: modelSourceMap.modelscope_value,
key: 'modelscope',
icon: <IconFont type="icon-tu2"></IconFont>,
onClick: (e: any) => {
setOpenDeployModal({
show: true,
width: 'calc(100vw - 220px)',
source: modelSourceMap.modelscope_value
});
}
}
];
@@ -222,9 +248,12 @@ const Models: React.FC<ModelsProps> = ({
const handleModalOk = useCallback(
async (data: FormData) => {
try {
console.log('data:', data, openDeployModal);
const result = getSourceRepoConfigValue(currentData?.source, data);
await updateModel({
data: {
...data
...result.values,
..._.omit(data, result.omits)
},
id: currentData?.id as number
});
@@ -252,7 +281,14 @@ const Models: React.FC<ModelsProps> = ({
try {
console.log('data:', data, openDeployModal);
await createModel({ data });
const result = getSourceRepoConfigValue(openDeployModal.source, data);
await createModel({
data: {
...result.values,
..._.omit(data, result.omits)
}
});
setOpenDeployModal({
...openDeployModal,
show: false
@@ -380,6 +416,16 @@ const Models: React.FC<ModelsProps> = ({
[workerList]
);
const generateSource = useCallback((record: ListItem) => {
if (record.source === modelSourceMap.modelscope_value) {
return `${modelSourceMap.modelScope} / ${record.model_scope_file_path}`;
}
if (record.source === modelSourceMap.huggingface_value) {
return `${modelSourceMap.huggingface} / ${record.huggingface_filename}`;
}
return `${modelSourceMap.ollama_library} / ${record.ollama_library_model_name}`;
}, []);
const handleCloseViewCode = useCallback(() => {
setEmbeddingParams({
params: {},
@@ -493,11 +539,7 @@ const Models: React.FC<ModelsProps> = ({
render={(text, record: ListItem) => {
return (
<span className="flex flex-column">
<span>
{record.source === modelSourceMap.huggingface_value
? `${modelSourceMap.huggingface} / ${record.huggingface_filename}`
: `${modelSourceMap.ollama_library} / ${record.ollama_library_model_name}`}
</span>
<span>{generateSource(record)}</span>
</span>
);
}}
+36 -19
View File
@@ -8,11 +8,11 @@ import { convertFileSize } from '@/utils';
import { useIntl } from '@umijs/max';
import { Form, Modal } from 'antd';
import _ from 'lodash';
import { memo, useEffect, useState } from 'react';
import { memo, useEffect, useMemo, useState } from 'react';
import SimpleBar from 'simplebar-react';
import 'simplebar-react/dist/simplebar.min.css';
import { queryGPUList, queryHuggingfaceModelFiles } from '../apis';
import { modelSourceMap } from '../config';
import { modelSourceMap, setSourceRepoConfigValue } from '../config';
import { FormData, GPUListItem, ListItem } from '../config/types';
import AdvanceConfig from './advance-config';
@@ -35,9 +35,19 @@ const sourceOptions = [
label: 'Ollama Library',
value: modelSourceMap.ollama_library_value,
key: 'ollama_library'
},
{
label: 'ModelScope',
value: modelSourceMap.modelscope_value,
key: 'model_scope'
}
];
const SEARCH_SOURCE = [
modelSourceMap.huggingface_value,
modelSourceMap.modelscope_value
];
const UpdateModal: React.FC<AddModalProps> = (props) => {
console.log('addmodel====');
const { title, action, open, onOk, onCancel } = props || {};
@@ -70,8 +80,13 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
});
}
if (action === PageAction.EDIT && open) {
const result = setSourceRepoConfigValue(
props.data?.source || '',
props.data
);
form.setFieldsValue({
...props.data,
...result.values,
..._.omit(props.data, result.omits),
scheduleType: props.data?.gpu_selector ? 'manual' : 'auto',
gpu_selector: props.data?.gpu_selector
? `${props.data?.gpu_selector.worker_name}-${props.data?.gpu_selector.gpu_name}-${props.data?.gpu_selector.gpu_index}`
@@ -118,7 +133,7 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
};
const handleRepoOnBlur = (e: any) => {
const repo = form.getFieldValue('huggingface_repo_id');
const repo = form.getFieldValue('repo_id');
handleFetchModelFiles(repo);
};
@@ -126,7 +141,7 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
return (
<>
<Form.Item<FormData>
name="huggingface_repo_id"
name="repo_id"
rules={[
{
required: true,
@@ -146,7 +161,7 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
></SealInput.Input>
</Form.Item>
<Form.Item<FormData>
name="huggingface_filename"
name="file_name"
rules={[
{
required: true,
@@ -165,7 +180,6 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
required
options={fileOptions}
loading={loading}
onFocus={handleRepoOnBlur}
disabled={action === PageAction.EDIT}
></SealAutoComplete>
</Form.Item>
@@ -229,18 +243,21 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
);
};
const renderFieldsBySource = () => {
switch (modelSource) {
case modelSourceMap.huggingface_value:
return renderHuggingfaceFields();
case modelSourceMap.ollama_library_value:
return renderOllamaModelFields();
case modelSourceMap.s3_value:
return renderS3Fields();
default:
return null;
const renderFieldsBySource = useMemo(() => {
if (SEARCH_SOURCE.includes(props.data?.source || '')) {
return renderHuggingfaceFields();
}
};
if (props.data?.source === modelSourceMap.ollama_library_value) {
return renderOllamaModelFields();
}
if (props.data?.source === modelSourceMap.s3_value) {
return renderS3Fields();
}
return null;
}, [props.data?.source]);
const handleSumit = () => {
form.submit();
@@ -367,7 +384,7 @@ const UpdateModal: React.FC<AddModalProps> = (props) => {
></SealSelect>
)}
</Form.Item>
{renderFieldsBySource()}
{renderFieldsBySource}
<Form.Item<FormData>
name="replicas"
rules={[
+75 -1
View File
@@ -87,7 +87,16 @@ export const modelSourceMap: Record<string, string> = {
s3: 'S3',
huggingface_value: 'huggingface',
ollama_library_value: 'ollama_library',
s3_value: 's3'
s3_value: 's3',
modelScope: 'ModelScope',
modelscope_value: 'model_scope'
};
export const modelSourceValueMap = {
[modelSourceMap.huggingface_value]: modelSourceMap.huggingface,
[modelSourceMap.ollama_library_value]: modelSourceMap.ollama_library,
[modelSourceMap.s3_value]: modelSourceMap.s3,
[modelSourceMap.modelscope_value]: modelSourceMap.modelScope
};
export const InstanceStatusMap = {
@@ -148,6 +157,13 @@ export const ModelSortType = {
lastModified: 'lastModified'
};
export const ModelScopeSortType = {
[ModelSortType.trendingScore]: 'Default',
[ModelSortType.likes]: 'StarsCount',
[ModelSortType.downloads]: 'DownloadsCount',
[ModelSortType.lastModified]: 'GmtModified'
};
export const placementStrategyOptions = [
{
label: 'Spread',
@@ -158,3 +174,61 @@ export const placementStrategyOptions = [
value: 'binpack'
}
];
export const sourceRepoConfig = {
[modelSourceMap.huggingface_value]: {
repo_id: 'huggingface_repo_id',
file_name: 'huggingface_filename'
},
[modelSourceMap.modelscope_value]: {
repo_id: 'model_scope_model_id',
file_name: 'model_scope_file_path'
}
};
export const getSourceRepoConfigValue = (
source: string,
data: any
): {
values: Record<string, any>;
omits: string[];
} => {
const config: Record<string, any> = sourceRepoConfig[source] || {};
const result: Record<string, any> = {};
const omits: string[] = [];
Object.keys(config)?.forEach((key: string) => {
if (config[key]) {
result[config[key]] = data[key];
omits.push(key);
}
});
return {
values: result,
omits: omits
};
};
export const setSourceRepoConfigValue = (
source: string,
data: any
): {
values: Record<string, any>;
omits: string[];
} => {
const config: Record<string, any> = sourceRepoConfig[source] || {};
const result: Record<string, any> = {};
const omits: string[] = [];
Object.keys(config)?.forEach((key: string) => {
if (config[key]) {
result[key] = data[config[key]];
omits.push(config[key]);
}
});
return {
values: result,
omits: omits
};
};
+6
View File
@@ -4,6 +4,8 @@ export interface ListItem {
huggingface_file_name: string;
huggingface_filename: string;
ollama_library_model_name: string;
model_scope_file_path: string;
model_scope_model_id: string;
embedding_only?: boolean;
ready_replicas: number;
replicas: number;
@@ -23,11 +25,15 @@ export interface ListItem {
export interface FormData {
source: string;
repo_id: string;
file_name: string;
huggingface_repo_id: string;
huggingface_filename: string;
s3_address: string;
ollama_library_model_name: 'string';
distributed_inference_across_workers?: boolean;
model_scope_model_id?: string;
model_scope_file_path?: string;
gpu_selector?: {
worker_name: string;
gpu_index: number;
@@ -26,7 +26,6 @@ import ReferenceParams from './reference-params';
import ViewCodeModal from './view-code-modal';
interface MessageProps {
parameters?: any;
modelList: Global.BaseOption<string>[];
ref?: any;
}
@@ -142,10 +141,6 @@ const GroundLeft: React.FC<MessageProps> = forwardRef((props, ref) => {
]
: [];
setMessageList((pre) => {
return [...pre, ...currentMessageRef.current];
});
contentRef.current = '';
const formatMessages = _.map(
[...messageList, ...currentMessageRef.current],
@@ -207,6 +202,9 @@ const GroundLeft: React.FC<MessageProps> = forwardRef((props, ref) => {
setLoading(false);
} catch (error) {
console.log('error=====', error);
setMessageList((pre) => {
return [...pre, ...currentMessageRef.current];
});
setLoading(false);
}
};
@@ -34,7 +34,6 @@ const MultiCompare: React.FC<MultiCompareProps> = ({ modelList }) => {
span: 12,
count: 2
});
const cacheModelInstanceList = useRef<any[]>([]);
const modelsCounterMap = useRef<Record<string, number>>({});
const modelRefs = useRef<any>({});
const boxHeight = 'calc(100vh - 72px)';
@@ -60,7 +60,7 @@ const ModelItem: React.FC<ModelItemProps> = forwardRef(
const [show, setShow] = useState(false);
const contentRef = useRef<any>('');
const controllerRef = useRef<any>(null);
const currentMessageRef = useRef<MessageItem>({} as MessageItem);
const currentMessageRef = useRef<MessageItem[]>([]);
const setMessageId = () => {
messageId.current = messageId.current + 1;
@@ -94,6 +94,7 @@ const ModelItem: React.FC<ModelItemProps> = forwardRef(
console.log('currentMessage==========5', messageList);
setMessageList([
...messageList,
...currentMessageRef.current,
{
role: Roles.Assistant,
content: contentRef.current,
@@ -102,13 +103,8 @@ const ModelItem: React.FC<ModelItemProps> = forwardRef(
]);
};
const submitMessage = async (currentParams: {
parameters: Record<string, any>;
currentMessage: Omit<MessageItem, 'uid'>;
}) => {
console.log('currentMessage==========3', currentParams);
const { parameters, currentMessage } = currentParams;
if (!parameters.model) return;
const submitMessage = async (currentMessage?: Omit<MessageItem, 'uid'>) => {
if (!params.model) return;
try {
setLoadingStatus(instanceId, true);
setMessageId();
@@ -116,26 +112,18 @@ const ModelItem: React.FC<ModelItemProps> = forwardRef(
controllerRef.current?.abort?.();
controllerRef.current = new AbortController();
const signal = controllerRef.current.signal;
currentMessageRef.current = {
...currentMessage,
uid: messageId.current
};
setMessageList((preList) => {
return [
...preList,
{
...currentMessageRef.current
}
];
});
currentMessageRef.current = currentMessage
? [
{
...currentMessage,
uid: messageId.current
}
]
: [];
console.log('currentMessageRef.current 1:', currentMessageRef.current);
console.log('currentMessage==========4', messageList);
const messages = _.map(
[
...messageList,
{
...currentMessageRef.current
}
],
[...messageList, ...currentMessageRef.current],
(item: MessageItem) => {
return {
role: item.role,
@@ -184,7 +172,7 @@ const ModelItem: React.FC<ModelItemProps> = forwardRef(
...formatMessages
]
: [...formatMessages],
...parameters,
...params,
stream: true
};
// ============== payload end ================
@@ -204,6 +192,9 @@ const ModelItem: React.FC<ModelItemProps> = forwardRef(
});
setLoadingStatus(instanceId, false);
} catch (error) {
setMessageList((preList) => {
return [...preList, ...currentMessageRef.current];
});
setLoadingStatus(instanceId, false);
}
};
@@ -222,7 +213,8 @@ const ModelItem: React.FC<ModelItemProps> = forwardRef(
content: string;
}) => {
console.log('currentMessage==========2', currentMessage);
submitMessage({ parameters: params, currentMessage });
const currentMsg = currentMessage.content ? currentMessage : undefined;
submitMessage(currentMsg);
};
const handleApplyToAllModels = (e: any) => {
@@ -260,7 +252,7 @@ const ModelItem: React.FC<ModelItemProps> = forwardRef(
setMessageList([]);
setTokenResult(null);
setSystemMessage('');
currentMessageRef.current = {} as MessageItem;
currentMessageRef.current = [];
console.log('clear message', systemMessage);
};
@@ -277,7 +269,7 @@ const ModelItem: React.FC<ModelItemProps> = forwardRef(
};
const handlePresetMessageList = (list: MessageItem[]) => {
currentMessageRef.current = {} as MessageItem;
currentMessageRef.current = [];
const messages = _.map(list, (item: Omit<MessageItem, 'uid'>) => {
setMessageId();
return {
+1 -1
View File
@@ -3,7 +3,7 @@ import { clearAtomStorage } from '@/atoms/utils';
import { RequestConfig, history } from '@umijs/max';
import { message } from 'antd';
const NoBaseURLAPIs = ['/auth', '/v1-openai', '/version'];
const NoBaseURLAPIs = ['/auth', '/v1-openai', '/version', '/proxy'];
export const requestConfig: RequestConfig = {
errorConfig: {