From d9319f57345ef53bd887341d8b220bbf529dab0e Mon Sep 17 00:00:00 2001 From: jialin Date: Thu, 2 Jan 2025 16:06:56 +0800 Subject: [PATCH] chore: catalog api --- config/routes.ts | 21 ++- src/components/icon-font/index.tsx | 2 +- src/locales/en-US/menu.ts | 4 +- src/locales/zh-CN/menu.ts | 4 +- src/pages/llmodels/apis/index.ts | 20 +++ src/pages/llmodels/catalog.tsx | 132 +++++++++----- .../llmodels/components/catalog-item.tsx | 81 ++++++--- src/pages/llmodels/components/data-form.tsx | 161 ++++++++++++------ .../components/deploy-builtin-modal.tsx | 46 ----- src/pages/llmodels/components/table-list.tsx | 13 +- src/pages/llmodels/config/test.ts | 84 --------- src/pages/llmodels/config/types.ts | 15 ++ src/pages/llmodels/style/catalog-item.less | 24 ++- 13 files changed, 313 insertions(+), 294 deletions(-) delete mode 100644 src/pages/llmodels/config/test.ts diff --git a/config/routes.ts b/config/routes.ts index 7ff3930f..1abe3e49 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -59,22 +59,21 @@ export default [ } ] }, + { + name: 'modelCatalog', + path: '/models/catalog', + key: 'modelsCatalog', + icon: 'icon-catalog', + access: 'canSeeAdmin', + component: './llmodels/catalog' + }, { name: 'models', - path: '/models', + path: '/models/list', key: 'models', icon: 'Block', access: 'canSeeAdmin', - component: './llmodels' - }, - { - name: 'modelsCatalog', - path: '/models/catalog', - key: 'modelsCatalog', - icon: 'Block', - access: 'canSeeAdmin', - hideInMenu: true, - component: './llmodels/catalog' + component: './llmodels/index' }, { name: 'resources', diff --git a/src/components/icon-font/index.tsx b/src/components/icon-font/index.tsx index 5a1cdf51..6d65adfc 100644 --- a/src/components/icon-font/index.tsx +++ b/src/components/icon-font/index.tsx @@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons'; // import './iconfont/iconfont.js'; const IconFont = createFromIconfontCN({ - scriptUrl: '//at.alicdn.com/t/c/font_4613488_bslq45omhet.js' + scriptUrl: '//at.alicdn.com/t/c/font_4613488_trmdczqx9z.js' }); export default IconFont; diff --git a/src/locales/en-US/menu.ts b/src/locales/en-US/menu.ts index 5eb451bb..da481597 100644 --- a/src/locales/en-US/menu.ts +++ b/src/locales/en-US/menu.ts @@ -8,8 +8,10 @@ export default { 'menu.playground.text2images': 'Image', 'menu.compare': 'Compare', 'menu.models': 'Models', + 'menu.models.modelList': 'Deploy & Manage', + 'menu.models.modelCatalog': 'Catalog', 'menu.models.catalog': 'Model Catalog', - 'menu.modelsCatalog': 'Model Catalog', + 'menu.modelCatalog': 'Catalog', 'menu.resources': 'Resources', 'menu.apikeys': 'API Keys', 'menu.users': 'Users', diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts index cd40366f..7ac59d79 100644 --- a/src/locales/zh-CN/menu.ts +++ b/src/locales/zh-CN/menu.ts @@ -8,8 +8,10 @@ export default { 'menu.playground.text2images': '文生图', 'menu.compare': '多模型对比', 'menu.models': '模型', + 'menu.models.modelList': '部署与管理', + 'menu.models.modelCatalog': '模型库', + 'menu.modelCatalog': '模型库', 'menu.models.catalog': '模型库', - 'menu.modelsCatalog': '模型库', 'menu.resources': '资源', 'menu.apikeys': 'API 密钥', 'menu.users': '用户', diff --git a/src/pages/llmodels/apis/index.ts b/src/pages/llmodels/apis/index.ts index 78f69b34..f93a4aa9 100644 --- a/src/pages/llmodels/apis/index.ts +++ b/src/pages/llmodels/apis/index.ts @@ -3,6 +3,7 @@ import { PipelineType } from '@huggingface/tasks'; import { request } from '@umijs/max'; import qs from 'query-string'; import { + CatalogItem, FormData, GPUListItem, ListItem, @@ -314,3 +315,22 @@ export async function downloadModelScopeModelfile( } return res.json(); } + +// ===================== catalog ===================== + +export async function queryCatalogList( + params: Global.SearchParams, + options?: any +) { + return request>(`/model-sets`, { + methos: 'GET', + ...options, + params + }); +} + +export async function queryCatalogItemSpec(id: number) { + return request(`/model-sets/${id}/specs`, { + method: 'GET' + }); +} diff --git a/src/pages/llmodels/catalog.tsx b/src/pages/llmodels/catalog.tsx index a5ca21e5..25a92c5a 100644 --- a/src/pages/llmodels/catalog.tsx +++ b/src/pages/llmodels/catalog.tsx @@ -8,48 +8,68 @@ import { Button, Col, Input, Pagination, Row, Space, message } from 'antd'; import _ from 'lodash'; import ResizeObserver from 'rc-resize-observer'; import React, { useCallback, useEffect, useState } from 'react'; -import { createModel, queryHuggingfaceModels } from './apis'; +import { createModel, queryCatalogList } from './apis'; import CatalogItem from './components/catalog-item'; import DelopyBuiltInModal from './components/deploy-builtin-modal'; import { getSourceRepoConfigValue, modelSourceMap } from './config'; -import testData from './config/test'; -import { FormData } from './config/types'; +import { CatalogItem as CatalogItemType, FormData } from './config/types'; const Catalog: React.FC = () => { const intl = useIntl(); const [span, setSpan] = React.useState(8); const [activeId, setActiveId] = React.useState(-1); + const [dataSource, setDataSource] = useState<{ + dataList: CatalogItemType[]; + loading: boolean; + total: number; + }>({ + dataList: [], + loading: false, + total: 0 + }); + const [queryParams, setQueryParams] = useState({ + page: 1, + perPage: 9, + search: '' + }); const [openDeployModal, setOpenDeployModal] = useState({ show: false, width: 600, source: modelSourceMap.huggingface_value }); - const [dataList, setDataList] = useState([]); - - const getModelsFromHuggingface = useCallback(async (sort: string) => { - console.log('getModelsFromHuggingface:', sort); + const fetchData = useCallback(async () => { + setDataSource((pre) => { + pre.loading = true; + return { ...pre }; + }); try { const params = { - search: { - query: 'gpustack', - tags: ['gguf'] - } + ..._.pickBy(queryParams, (val: any) => !!val) }; - const data = await queryHuggingfaceModels(params); - let list = _.map(data || [], (item: any) => { - return { - ...item, - value: item.name, - label: item.name - }; + const res = await queryCatalogList(params); + + setDataSource({ + dataList: res.items, + loading: false, + total: res.pagination.total }); - setDataList(list); } catch (error) { - console.log('error:', error); - return []; + setDataSource({ + dataList: [], + loading: false, + total: dataSource.total + }); + console.log('error', error); } - }, []); + }, [queryParams]); + + const handleDeployModalCancel = () => { + setOpenDeployModal({ + ...openDeployModal, + show: false + }); + }; const handleResize = useCallback( _.throttle((size: { width: number; height: number }) => { @@ -65,29 +85,18 @@ const Catalog: React.FC = () => { } else { setSpan(8); } - console.log('size:', size); }, 100), [] ); - const handleDeployModalCancel = () => { + const handleOnDeploy = useCallback((item: CatalogItemType) => { + setActiveId(item.id); setOpenDeployModal({ - ...openDeployModal, - show: false + show: true, + source: modelSourceMap.huggingface_value, + width: 600 }); - }; - - const handleOnDeploy = useCallback( - (index: number) => { - setActiveId(index); - setOpenDeployModal({ - show: true, - source: modelSourceMap.huggingface_value, - width: 600 - }); - }, - [openDeployModal] - ); + }, []); const handleCreateModel = useCallback( async (data: FormData) => { @@ -112,14 +121,34 @@ const Catalog: React.FC = () => { [openDeployModal] ); + const handleOnPageChange = useCallback((page: number, pageSize?: number) => { + setQueryParams({ + ...queryParams, + page, + perPage: pageSize || 10 + }); + }, []); + + const handleSearch = (e: any) => { + fetchData(); + }; + + const handleNameChange = (e: any) => { + setQueryParams({ + ...queryParams, + search: e.target.value + }); + }; + useEffect(() => { - getModelsFromHuggingface('desc'); - }, [getModelsFromHuggingface]); + fetchData(); + }, [queryParams]); + return ( { style={{ width: 300 }} size="large" allowClear + onChange={handleNameChange} > } > - {testData.map((item, index) => { + {dataSource.dataList.map((item: CatalogItemType, index) => { return ( - + handleOnDeploy(index)} + onClick={handleOnDeploy} activeId={activeId} data={item} - itemId={index} > ); @@ -159,7 +189,15 @@ const Catalog: React.FC = () => {
- +
void; + data: CatalogItemType; + onClick: (data: CatalogItemType) => void; } const CatalogItem: React.FC = (props) => { - const { onDeploy, itemId, activeId, data } = props; + const { onClick, activeId, data } = props; - const handleOnDeploy = () => { - onDeploy(); + const handleOnClick = useCallback(() => { + onClick(data); + }, [data, onClick]); + + const icon = useMemo(() => { + const home = data.home?.replace(/\/$/, ''); + const icon = data.icon?.replace(/^\//, ''); + if (icon) { + return `${home}/${icon}`; + } + return IMG; + }, [data]); + + const handleOnError = (e: any) => { + e.target.src = IMG; }; + return (
- +
{data.name} @@ -36,34 +54,43 @@ const CatalogItem: React.FC = (props) => {
+
+ + + {data.release_date} + + + + {_.map(data.licenses, (license: string, index: number) => { + return ( + <> + {license} + {index !== data.licenses.length - 1 && ( + + )} + + ); + })} + +
- {data.tags.map((sItem, i) => { + {data.categories.map((sItem, i) => { return ( {sItem} ); })} - {/* - Audio - - - GGUF - - - Qwen - */} - {/* - Audio - GGUF - Qwen */} - {data.size?.length > 0 && ( + {data.sizes?.length > 0 && ( <> - {data.size.map((sItem, i) => { + {data.sizes.map((sItem, i) => { return ( - {sItem} + {sItem}B ); })} diff --git a/src/pages/llmodels/components/data-form.tsx b/src/pages/llmodels/components/data-form.tsx index cba02eed..37766a58 100644 --- a/src/pages/llmodels/components/data-form.tsx +++ b/src/pages/llmodels/components/data-form.tsx @@ -35,6 +35,11 @@ interface DataFormProps { action: PageActionType; selectedModel: any; isGGUF: boolean; + sizeOptions?: Global.BaseOption[]; + quantizationOptions?: Global.BaseOption[]; + sourceDisable?: boolean; + byBuiltIn?: boolean; + backendOptions?: Global.BaseOption[]; onOk: (values: FormData) => void; onBackendChange?: (value: string) => void; } @@ -69,7 +74,14 @@ const sourceOptions = [ ]; const DataForm: React.FC = forwardRef((props, ref) => { - const { action, isGGUF, onOk } = props; + const { + action, + isGGUF, + sourceDisable = true, + backendOptions, + byBuiltIn, + onOk + } = props; const [form] = Form.useForm(); const intl = useIntl(); const [gpuOptions, setGpuOptions] = useState< @@ -259,34 +271,6 @@ const DataForm: React.FC = forwardRef((props, ref) => { ); }; - const renderS3Fields = () => { - return ( - <> - - name="s3_address" - rules={[ - { - required: true, - message: intl.formatMessage( - { - id: 'common.form.rule.input' - }, - { name: intl.formatMessage({ id: 'models.form.s3address' }) } - ) - } - ]} - > - - - - ); - }; - const renderOllamaModelFields = () => { return ( <> @@ -364,6 +348,71 @@ const DataForm: React.FC = forwardRef((props, ref) => { ); }; + const handleSizeChange = (val: any) => { + form.setFieldsValue({ + quantization: '' + }); + }; + + const renderFieldsFromCatalog = useMemo(() => { + if (!byBuiltIn) { + return null; + } + return ( + <> + + name="size" + key="size" + rules={[ + { + required: true, + message: intl.formatMessage( + { + id: 'common.form.rule.select' + }, + { name: 'size' } + ) + } + ]} + > + + + + name="quantization" + key="quantization" + rules={[ + { + required: true, + message: intl.formatMessage( + { + id: 'common.form.rule.select' + }, + { name: 'quantization' } + ) + } + ]} + > + + + + ); + }, [props.sizeOptions, props.quantizationOptions, byBuiltIn]); + const renderFieldsBySource = useMemo(() => { if (SEARCH_SOURCE.includes(props.source)) { return renderHuggingfaceFields(); @@ -373,9 +422,6 @@ const DataForm: React.FC = forwardRef((props, ref) => { return renderOllamaModelFields(); } - if (props.source === modelSourceMap.s3_value) { - return renderS3Fields(); - } if (props.source === modelSourceMap.local_path_value) { return renderLocalPathFields(); } @@ -477,7 +523,7 @@ const DataForm: React.FC = forwardRef((props, ref) => { > { = forwardRef((props, ref) => {
} - options={[ - { - label: `llama-box`, - value: backendOptionsMap.llamaBox, - disabled: - props.source === modelSourceMap.local_path_value - ? false - : !isGGUF - }, - { - label: 'vLLM', - value: backendOptionsMap.vllm, - disabled: - props.source === modelSourceMap.local_path_value - ? false - : isGGUF - }, - { - label: 'vox-box', - value: backendOptionsMap.voxBox, - disabled: props.source === modelSourceMap.ollama_library_value - } - ]} + options={ + backendOptions ?? [ + { + label: `llama-box`, + value: backendOptionsMap.llamaBox, + disabled: + props.source === modelSourceMap.local_path_value + ? false + : !isGGUF + }, + { + label: 'vLLM', + value: backendOptionsMap.vllm, + disabled: + props.source === modelSourceMap.local_path_value + ? false + : isGGUF + }, + { + label: 'vox-box', + value: backendOptionsMap.voxBox, + disabled: props.source === modelSourceMap.ollama_library_value + } + ] + } disabled={ action === PageAction.EDIT && props.source !== modelSourceMap.local_path_value } > + {renderFieldsFromCatalog} name="description"> = (props) => { footer={false} >
- {SEARCH_SOURCE.includes(props.source) && ( - <> -
- - - - -
-
- - - {isGGUF && ( - - )} - - -
- - )} = (props) => { } > <> - {SEARCH_SOURCE.includes(source) && ( - - {intl.formatMessage({ id: 'models.form.configurations' })} - - - )} = ({ }, [deleteIds]); const sourceOptions = [ - { - label: 'Model Catalog', - value: 'model_catalog', - key: 'model_catalog', - icon: - }, { label: 'Hugging Face', value: modelSourceMap.huggingface_value, @@ -688,9 +681,6 @@ const Models: React.FC = ({ source: modelSourceMap.huggingface_value }); } - if (item.key === 'model_catalog') { - navigate('/models/catalog'); - } if (item.key === 'ollama_library') { setOpenDeployModal({ @@ -722,7 +712,8 @@ const Models: React.FC = ({ diff --git a/src/pages/llmodels/config/test.ts b/src/pages/llmodels/config/test.ts deleted file mode 100644 index 966a254a..00000000 --- a/src/pages/llmodels/config/test.ts +++ /dev/null @@ -1,84 +0,0 @@ -export default [ - { - name: 'gpustack/bge-m3-GGUF', - tags: ['Embedding', 'GGUF', 'bert'], - description: - 'It can simultaneously perform the three common retrieval functionalities of embedding model: dense retrieval, multi-vector retrieval, and sparse retrieval.', - size: ['567M'] - }, - { - name: 'gpustack/stable-diffusion-v3-5-medium-GGUF', - tags: ['Text-to-Image', 'GGUF'], - description: - 'Stable Diffusion 3.5 Medium is a Multimodal Diffusion Transformer with improvements (MMDiT-X) text-to-image model that features improved performance in image quality, typography, complex prompt understanding, and resource-efficiency.', - size: ['8B'] - }, - { - name: 'gpustack/stable-diffusion-v3-5-large-GGUF', - tags: ['Text-to-Image', 'GGUF'], - description: - 'Stable Diffusion 3.5 Large is a Multimodal Diffusion Transformer (MMDiT) text-to-image model that features improved performance in image quality, typography, complex prompt understanding, and resource-efficiency', - size: ['14B'] - }, - { - name: 'gpustack/jina-reranker-v1-tiny-en-GGUF', - tags: ['Rerank', 'GGUF', 'jina-bert-v2'], - description: - 'his model is designed for blazing-fast reranking while maintaining competitive performance.', - size: ['Q8_0', 'Q4_0'] - }, - { - name: 'gpustack/stable-diffusion-v1-5-inpainting-GGUF', - tags: ['Text-to-Image', 'GGUF'], - description: - 'Stable Diffusion Inpainting is a latent text-to-image diffusion model capable of generating photo-realistic images given any text input, with the extra capability of inpainting the pictures by using a mask', - size: ['1B', 'Q8_0', 'Q4_0'] - }, - { - name: 'gpustack/Llama-3.2-3B-Instruct-GGUF', - tags: ['Text-to-Text', 'GGUF', 'llama'], - description: - 'The Llama 3.2 collection of multilingual large language models (LLMs) is a collection of pretrained and instruction-tuned generative models in 1B and 3B sizes (text in/text out).', - size: ['1B', 'Q8_0', 'Q4_0'] - }, - { - name: 'gpustack/faster-whisper-large-v1', - description: - 'This model can be used in CTranslate2 or projects based on CTranslate2 such as faster-whisper.', - tags: ['Speech-to-Text'], - size: [] - }, - { - name: 'gpustack/faster-distil-whisper-medium.en', - description: - 'This model can be used in CTranslate2 or projects based on CTranslate2 such as faster-whisper.', - tags: ['Speech-to-Text'] - }, - { - name: 'gpustack/CosyVoice-300M', - description: - 'This model is a lightweight, fast, and efficient text-to-speech model that can be used in various applications.', - tags: ['Text-to-Speech'] - }, - { - name: 'gpustack/FLUX.1-mini-GGUF', - tags: ['Text-to-Image', 'GGUF'], - description: - 'A 3.2B MMDiT distilled from Flux-dev for efficient text-to-image generation', - size: ['8B', 'Q8_0', 'Q4_0', 'FP16'] - }, - { - name: 'gpustack/stable-diffusion-v3-5-large-turbo-GGUF', - tags: ['Text-to-Image', 'GGUF'], - description: - 'Stable Diffusion 3.5 Large Turbo is a Multimodal Diffusion Transformer (MMDiT) text-to-image model with Adversarial Diffusion Distillation (ADD)', - size: ['14B', 'Q8_0', 'Q4_0'] - }, - { - name: 'gpustack/FLUX.1-lite-GGUF', - description: - 'This version uses 7 GB less RAM and runs 23% faster while maintaining the same precision (bfloat16) as the original model', - tags: ['Text-to-Image', 'GGUF'], - size: ['14B', 'Q8_0', 'Q4_0'] - } -]; diff --git a/src/pages/llmodels/config/types.ts b/src/pages/llmodels/config/types.ts index 44a5b519..3a973424 100644 --- a/src/pages/llmodels/config/types.ts +++ b/src/pages/llmodels/config/types.ts @@ -31,6 +31,8 @@ export interface ListItem { export interface FormData { backend?: string; + size?: number; + quantization?: number; categories?: string[]; backend_parameters?: string[]; backend_version?: string; @@ -118,3 +120,16 @@ export interface GPUListItem { worker_name: string; worker_ip: string; } + +export interface CatalogItem { + name: string; + id: number; + description: string; + home: string; + icon: string; + categories: string[]; + capabilities: string[]; + sizes: number[]; + licenses: string[]; + release_date: string; +} diff --git a/src/pages/llmodels/style/catalog-item.less b/src/pages/llmodels/style/catalog-item.less index d420c585..5bcc8d68 100644 --- a/src/pages/llmodels/style/catalog-item.less +++ b/src/pages/llmodels/style/catalog-item.less @@ -1,5 +1,5 @@ .catalog-item { - height: 160px; + height: 180px; overflow: hidden; display: flex; padding: 16px 20px; @@ -31,7 +31,6 @@ justify-content: flex-start; flex: 1; width: 100%; - // height: 100%; } &:hover { @@ -48,8 +47,8 @@ display: flex; align-items: center; justify-content: flex-start; - gap: 8px; - margin-bottom: 16px; + gap: 12px; + margin-bottom: 12px; .name { flex: 1; @@ -61,23 +60,30 @@ color: var(--ant-color-text-secondary); width: 100%; overflow: hidden; - margin-bottom: 16px; + margin-bottom: 12px; } .item-footer { width: 100%; display: flex; flex-direction: column; - gap: 8px; + gap: 12px; justify-content: flex-start; align-items: flex-start; } + .update-time { + color: var(--ant-color-text-tertiary); + display: flex; + align-items: center; + gap: 16px; + } + .dot { - width: 4px; - height: 4px; + width: 3px; + height: 3px; border-radius: 50%; - background-color: var(--ant-color-text-tertiary); + background-color: var(--ant-color-text-quaternary); margin-right: 8px; }