mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-25 03:10:08 +08:00
refactor(ModelList): improve model list style and grouping (#5674)
* refactor(ModelList): make the group removing button consistent with model removing button * refactor(ModelList): improve auto-grouping, reduce the number of groups * refactor(EditModelsPopup): extract model group tools * refactor(naming): add special grouping rules for some providers * feat(ModelList): add a button to add/remove all the listed models * refactor(naming): update auto grouping * refactor: update auto grouping for dmxapi * revert: remove ungrouped
This commit is contained in:
parent
4c3c863c7d
commit
83cea0750d
@ -1389,6 +1389,8 @@
|
||||
"models.default_assistant_model_description": "Model used when creating a new assistant, if the assistant is not set, this model will be used",
|
||||
"models.empty": "No models found",
|
||||
"models.enable_topic_naming": "Topic Auto Naming",
|
||||
"models.manage.add_listed": "Add models to the list",
|
||||
"models.manage.remove_listed": "Remove models from the list",
|
||||
"models.manage.add_whole_group": "Add the whole group",
|
||||
"models.manage.remove_whole_group": "Remove the whole group",
|
||||
"models.topic_naming_model": "Topic Naming Model",
|
||||
|
||||
@ -1387,6 +1387,8 @@
|
||||
"models.default_assistant_model_description": "新しいアシスタントを作成する際に使用されるモデル。アシスタントがモデルを設定していない場合、このモデルが使用されます",
|
||||
"models.empty": "モデルが見つかりません",
|
||||
"models.enable_topic_naming": "トピックの自動命名",
|
||||
"models.manage.add_listed": "リストにモデルを追加",
|
||||
"models.manage.remove_listed": "リストからモデルを削除",
|
||||
"models.manage.add_whole_group": "グループ全体を追加",
|
||||
"models.manage.remove_whole_group": "グループ全体を削除",
|
||||
"models.topic_naming_model": "トピック命名モデル",
|
||||
|
||||
@ -1387,6 +1387,8 @@
|
||||
"models.default_assistant_model_description": "Модель, используемая при создании нового ассистента, если ассистент не имеет настроенной модели, будет использоваться эта модель",
|
||||
"models.empty": "Модели не найдены",
|
||||
"models.enable_topic_naming": "Автоматическое переименование топика",
|
||||
"models.manage.add_listed": "Добавить в список",
|
||||
"models.manage.remove_listed": "Удалить из списка",
|
||||
"models.manage.add_whole_group": "Добавить всю группу",
|
||||
"models.manage.remove_whole_group": "Удалить всю группу",
|
||||
"models.topic_naming_model": "Модель именования топика",
|
||||
|
||||
@ -1389,6 +1389,8 @@
|
||||
"models.default_assistant_model_description": "创建新助手时使用的模型,如果助手未设置模型,则使用此模型",
|
||||
"models.empty": "没有模型",
|
||||
"models.enable_topic_naming": "话题自动重命名",
|
||||
"models.manage.add_listed": "添加列表中的模型",
|
||||
"models.manage.remove_listed": "移除列表中的模型",
|
||||
"models.manage.add_whole_group": "添加整个分组",
|
||||
"models.manage.remove_whole_group": "移除整个分组",
|
||||
"models.topic_naming_model": "话题命名模型",
|
||||
|
||||
@ -1388,6 +1388,8 @@
|
||||
"models.default_assistant_model_description": "建立新助手時使用的模型,如果助手未設定模型,則使用此模型",
|
||||
"models.empty": "找不到模型",
|
||||
"models.enable_topic_naming": "話題自動重新命名",
|
||||
"models.manage.add_listed": "添加列表中的模型",
|
||||
"models.manage.remove_listed": "移除列表中的模型",
|
||||
"models.manage.add_whole_group": "新增整個分組",
|
||||
"models.manage.remove_whole_group": "移除整個分組",
|
||||
"models.topic_naming_model": "話題命名模型",
|
||||
|
||||
@ -104,7 +104,7 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve }) => {
|
||||
maxLength={200}
|
||||
onChange={(e) => {
|
||||
form.setFieldValue('name', e.target.value)
|
||||
form.setFieldValue('group', getDefaultGroupName(e.target.value))
|
||||
form.setFieldValue('group', getDefaultGroupName(e.target.value, provider.id))
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@ -22,7 +22,7 @@ import { Avatar, Button, Empty, Flex, Modal, Tabs, Tooltip, Typography } from 'a
|
||||
import Input from 'antd/es/input/Input'
|
||||
import { groupBy, isEmpty, uniqBy } from 'lodash'
|
||||
import { Search } from 'lucide-react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@ -83,39 +83,36 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
}
|
||||
})
|
||||
|
||||
const modelGroups =
|
||||
provider.id === 'dashscope'
|
||||
? {
|
||||
...groupBy(
|
||||
list.filter((model) => !model.id.startsWith('qwen')),
|
||||
'group'
|
||||
),
|
||||
...groupQwenModels(list.filter((model) => model.id.startsWith('qwen')))
|
||||
}
|
||||
: groupBy(list, 'group')
|
||||
const modelGroups = useMemo(
|
||||
() =>
|
||||
provider.id === 'dashscope'
|
||||
? {
|
||||
...groupBy(
|
||||
list.filter((model) => !model.id.startsWith('qwen')),
|
||||
'group'
|
||||
),
|
||||
...groupQwenModels(list.filter((model) => model.id.startsWith('qwen')))
|
||||
}
|
||||
: groupBy(list, 'group'),
|
||||
[list, provider.id]
|
||||
)
|
||||
|
||||
const onOk = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
const onOk = useCallback(() => setOpen(false), [])
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
const onCancel = useCallback(() => setOpen(false), [])
|
||||
|
||||
const onClose = () => {
|
||||
resolve({})
|
||||
}
|
||||
const onClose = useCallback(() => resolve({}), [resolve])
|
||||
|
||||
const onAddModel = (model: Model) => {
|
||||
if (isEmpty(model.name)) {
|
||||
return
|
||||
}
|
||||
addModel(model)
|
||||
}
|
||||
const onAddModel = useCallback(
|
||||
(model: Model) => {
|
||||
if (!isEmpty(model.name)) {
|
||||
addModel(model)
|
||||
}
|
||||
},
|
||||
[addModel]
|
||||
)
|
||||
|
||||
const onRemoveModel = (model: Model) => {
|
||||
removeModel(model)
|
||||
}
|
||||
const onRemoveModel = useCallback((model: Model) => removeModel(model), [removeModel])
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
@ -129,7 +126,7 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
// @ts-ignore name
|
||||
name: model.name || model.id,
|
||||
provider: _provider.id,
|
||||
group: getDefaultGroupName(model.id),
|
||||
group: getDefaultGroupName(model.id, _provider.id),
|
||||
// @ts-ignore name
|
||||
description: model?.description,
|
||||
owned_by: model?.owned_by
|
||||
@ -165,6 +162,63 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const renderTopTools = useCallback(() => {
|
||||
const isAllFilteredInProvider = list.length > 0 && list.every((model) => isModelInProvider(provider, model.id))
|
||||
return (
|
||||
<Tooltip
|
||||
destroyTooltipOnHide
|
||||
title={
|
||||
isAllFilteredInProvider ? t('settings.models.manage.remove_listed') : t('settings.models.manage.add_listed')
|
||||
}
|
||||
placement="top">
|
||||
<Button
|
||||
type={isAllFilteredInProvider ? 'default' : 'primary'}
|
||||
icon={isAllFilteredInProvider ? <MinusOutlined /> : <PlusOutlined />}
|
||||
size="large"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (isAllFilteredInProvider) {
|
||||
list.filter((model) => isModelInProvider(provider, model.id)).forEach(onRemoveModel)
|
||||
} else {
|
||||
list.filter((model) => !isModelInProvider(provider, model.id)).forEach(onAddModel)
|
||||
}
|
||||
}}
|
||||
disabled={list.length === 0}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
}, [list, provider, onAddModel, onRemoveModel, t])
|
||||
|
||||
const renderGroupTools = useCallback(
|
||||
(group: string) => {
|
||||
const isAllInProvider = modelGroups[group].every((model) => isModelInProvider(provider, model.id))
|
||||
return (
|
||||
<Tooltip
|
||||
destroyTooltipOnHide
|
||||
title={
|
||||
isAllInProvider
|
||||
? t(`settings.models.manage.remove_whole_group`)
|
||||
: t(`settings.models.manage.add_whole_group`)
|
||||
}
|
||||
placement="top">
|
||||
<Button
|
||||
type="text"
|
||||
icon={isAllInProvider ? <MinusOutlined /> : <PlusOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (isAllInProvider) {
|
||||
modelGroups[group].filter((model) => isModelInProvider(provider, model.id)).forEach(onRemoveModel)
|
||||
} else {
|
||||
modelGroups[group].filter((model) => !isModelInProvider(provider, model.id)).forEach(onAddModel)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
[modelGroups, provider, onRemoveModel, onAddModel, t]
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<ModalHeader />}
|
||||
@ -180,14 +234,17 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
}}
|
||||
centered>
|
||||
<SearchContainer>
|
||||
<Input
|
||||
prefix={<Search size={14} />}
|
||||
size="large"
|
||||
ref={searchInputRef}
|
||||
placeholder={t('settings.provider.search_placeholder')}
|
||||
allowClear
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
/>
|
||||
<TopToolsWrapper>
|
||||
<Input
|
||||
prefix={<Search size={14} />}
|
||||
size="large"
|
||||
ref={searchInputRef}
|
||||
placeholder={t('settings.provider.search_placeholder')}
|
||||
allowClear
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
/>
|
||||
{renderTopTools()}
|
||||
</TopToolsWrapper>
|
||||
<Tabs
|
||||
size={i18n.language.startsWith('zh') ? 'middle' : 'small'}
|
||||
defaultActiveKey="all"
|
||||
@ -206,7 +263,6 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
</SearchContainer>
|
||||
<ListContainer>
|
||||
{Object.keys(modelGroups).map((group, i) => {
|
||||
const isAllInProvider = modelGroups[group].every((model) => isModelInProvider(provider, model.id))
|
||||
return (
|
||||
<CustomCollapse
|
||||
key={i}
|
||||
@ -220,31 +276,7 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
</CustomTag>
|
||||
</Flex>
|
||||
}
|
||||
extra={
|
||||
<Tooltip
|
||||
destroyTooltipOnHide
|
||||
title={
|
||||
isAllInProvider
|
||||
? t(`settings.models.manage.remove_whole_group`)
|
||||
: t(`settings.models.manage.add_whole_group`)
|
||||
}
|
||||
placement="top">
|
||||
<Button
|
||||
type="text"
|
||||
icon={isAllInProvider ? <MinusOutlined /> : <PlusOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (isAllInProvider) {
|
||||
modelGroups[group]
|
||||
.filter((model) => isModelInProvider(provider, model.id))
|
||||
.forEach(onRemoveModel)
|
||||
} else {
|
||||
modelGroups[group].filter((model) => !isModelInProvider(provider, model.id)).forEach(onAddModel)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
}>
|
||||
extra={renderGroupTools(group)}>
|
||||
<FlexColumn style={{ margin: '10px 0' }}>
|
||||
{modelGroups[group].map((model) => (
|
||||
<FileItem
|
||||
@ -325,6 +357,12 @@ const SearchContainer = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const TopToolsWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const ListContainer = styled.div`
|
||||
height: calc(100vh - 300px);
|
||||
overflow-y: scroll;
|
||||
|
||||
@ -3,7 +3,6 @@ import {
|
||||
CloseCircleFilled,
|
||||
ExclamationCircleFilled,
|
||||
LoadingOutlined,
|
||||
MinusCircleOutlined,
|
||||
MinusOutlined,
|
||||
PlusOutlined
|
||||
} from '@ant-design/icons'
|
||||
@ -244,77 +243,81 @@ const ModelList: React.FC<ModelListProps> = ({ providerId, modelStatuses = [], s
|
||||
<>
|
||||
<Flex gap={12} vertical>
|
||||
{Object.keys(sortedModelGroups).map((group, i) => (
|
||||
<CustomCollapse
|
||||
defaultActiveKey={i <= 5 ? ['1'] : []}
|
||||
key={group}
|
||||
label={
|
||||
<Flex align="center" gap={10}>
|
||||
<span style={{ fontWeight: 600 }}>{group}</span>
|
||||
</Flex>
|
||||
}
|
||||
extra={
|
||||
<Tooltip title={t('settings.models.manage.remove_whole_group')}>
|
||||
<HoveredRemoveIcon
|
||||
onClick={() =>
|
||||
modelGroups[group]
|
||||
.filter((model) => provider.models.some((m) => m.id === model.id))
|
||||
.forEach((model) => removeModel(model))
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
}>
|
||||
<Flex gap={10} vertical style={{ marginTop: 10 }}>
|
||||
{sortedModelGroups[group].map((model) => {
|
||||
const modelStatus = modelStatuses.find((status) => status.model.id === model.id)
|
||||
const isChecking = modelStatus?.checking === true
|
||||
<CustomCollapseWrapper key={group}>
|
||||
<CustomCollapse
|
||||
defaultActiveKey={i <= 5 ? ['1'] : []}
|
||||
label={
|
||||
<Flex align="center" gap={10}>
|
||||
<span style={{ fontWeight: 600 }}>{group}</span>
|
||||
</Flex>
|
||||
}
|
||||
extra={
|
||||
<Tooltip title={t('settings.models.manage.remove_whole_group')}>
|
||||
<Button
|
||||
type="text"
|
||||
className="toolbar-item"
|
||||
icon={<MinusOutlined />}
|
||||
onClick={() =>
|
||||
modelGroups[group]
|
||||
.filter((model) => provider.models.some((m) => m.id === model.id))
|
||||
.forEach((model) => removeModel(model))
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
}>
|
||||
<Flex gap={10} vertical style={{ marginTop: 10 }}>
|
||||
{sortedModelGroups[group].map((model) => {
|
||||
const modelStatus = modelStatuses.find((status) => status.model.id === model.id)
|
||||
const isChecking = modelStatus?.checking === true
|
||||
|
||||
return (
|
||||
<ListItem key={model.id}>
|
||||
<HStack alignItems="center" gap={10} style={{ flex: 1 }}>
|
||||
<Avatar src={getModelLogo(model.id)} style={{ width: 26, height: 26 }}>
|
||||
{model?.name?.[0]?.toUpperCase()}
|
||||
</Avatar>
|
||||
<ListItemName>
|
||||
<Tooltip
|
||||
styles={{
|
||||
root: {
|
||||
width: 'auto',
|
||||
maxWidth: '500px'
|
||||
return (
|
||||
<ListItem key={model.id}>
|
||||
<HStack alignItems="center" gap={10} style={{ flex: 1 }}>
|
||||
<Avatar src={getModelLogo(model.id)} style={{ width: 26, height: 26 }}>
|
||||
{model?.name?.[0]?.toUpperCase()}
|
||||
</Avatar>
|
||||
<ListItemName>
|
||||
<Tooltip
|
||||
styles={{
|
||||
root: {
|
||||
width: 'auto',
|
||||
maxWidth: '500px'
|
||||
}
|
||||
}}
|
||||
destroyTooltipOnHide
|
||||
title={
|
||||
<Typography.Text style={{ color: 'white' }} copyable={{ text: model.id }}>
|
||||
{model.id}
|
||||
</Typography.Text>
|
||||
}
|
||||
}}
|
||||
destroyTooltipOnHide
|
||||
title={
|
||||
<Typography.Text style={{ color: 'white' }} copyable={{ text: model.id }}>
|
||||
{model.id}
|
||||
</Typography.Text>
|
||||
}
|
||||
placement="top">
|
||||
<NameSpan>{model.name}</NameSpan>
|
||||
</Tooltip>
|
||||
<ModelTagsWithLabel model={model} size={11} style={{ flexShrink: 0 }} />
|
||||
</ListItemName>
|
||||
</HStack>
|
||||
<Flex gap={4} align="center">
|
||||
{renderLatencyText(modelStatus)}
|
||||
{renderStatusIndicator(modelStatus)}
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => !isChecking && onEditModel(model)}
|
||||
disabled={isChecking}
|
||||
icon={<Bolt size={16} />}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => !isChecking && removeModel(model)}
|
||||
disabled={isChecking}
|
||||
icon={<MinusOutlined />}
|
||||
/>
|
||||
</Flex>
|
||||
</ListItem>
|
||||
)
|
||||
})}
|
||||
</Flex>
|
||||
</CustomCollapse>
|
||||
placement="top">
|
||||
<NameSpan>{model.name}</NameSpan>
|
||||
</Tooltip>
|
||||
<ModelTagsWithLabel model={model} size={11} style={{ flexShrink: 0 }} />
|
||||
</ListItemName>
|
||||
</HStack>
|
||||
<Flex gap={4} align="center">
|
||||
{renderLatencyText(modelStatus)}
|
||||
{renderStatusIndicator(modelStatus)}
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => !isChecking && onEditModel(model)}
|
||||
disabled={isChecking}
|
||||
icon={<Bolt size={16} />}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => !isChecking && removeModel(model)}
|
||||
disabled={isChecking}
|
||||
icon={<MinusOutlined />}
|
||||
/>
|
||||
</Flex>
|
||||
</ListItem>
|
||||
)
|
||||
})}
|
||||
</Flex>
|
||||
</CustomCollapse>
|
||||
</CustomCollapseWrapper>
|
||||
))}
|
||||
{docsWebsite && (
|
||||
<SettingHelpTextRow>
|
||||
@ -352,6 +355,19 @@ const ModelList: React.FC<ModelListProps> = ({ providerId, modelStatuses = [], s
|
||||
)
|
||||
}
|
||||
|
||||
const CustomCollapseWrapper = styled.div`
|
||||
.toolbar-item {
|
||||
margin-top: 2px;
|
||||
transform: translateZ(0);
|
||||
will-change: opacity;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
&:hover .toolbar-item {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const ListItem = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -387,24 +403,6 @@ const NameSpan = styled.span`
|
||||
font-size: 14px;
|
||||
`
|
||||
|
||||
const RemoveIcon = styled(MinusCircleOutlined)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
color: var(--color-error);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
`
|
||||
|
||||
const HoveredRemoveIcon = styled(RemoveIcon)`
|
||||
opacity: 0;
|
||||
margin-top: 2px;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const StatusIndicator = styled.div<{ type: string }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -115,14 +115,45 @@ describe('naming', () => {
|
||||
expect(getDefaultGroupName('group:model')).toBe('group')
|
||||
})
|
||||
|
||||
it('should extract group name from ID with space', () => {
|
||||
// 验证从包含空格的 ID 中提取组名
|
||||
expect(getDefaultGroupName('foo bar')).toBe('foo')
|
||||
})
|
||||
|
||||
it('should extract group name from ID with hyphen', () => {
|
||||
// 验证从包含连字符的 ID 中提取组名
|
||||
expect(getDefaultGroupName('group-subgroup-model')).toBe('group-subgroup')
|
||||
})
|
||||
|
||||
it('should return original ID if no separators', () => {
|
||||
// 验证没有分隔符时返回原始 ID
|
||||
expect(getDefaultGroupName('group')).toBe('group')
|
||||
it('should use first delimiters for special providers', () => {
|
||||
// 这些 provider 下,'/', ' ', '-', '_', ':' 都属于第一类分隔符,分割后取第0部分
|
||||
const specialProviders = ['aihubmix', 'silicon', 'ocoolai', 'o3', 'dmxapi']
|
||||
specialProviders.forEach((provider) => {
|
||||
expect(getDefaultGroupName('Qwen/Qwen3-32B', provider)).toBe('qwen')
|
||||
expect(getDefaultGroupName('gpt-4.1-mini', provider)).toBe('gpt')
|
||||
expect(getDefaultGroupName('gpt-4.1', provider)).toBe('gpt')
|
||||
expect(getDefaultGroupName('gpt_4.1', provider)).toBe('gpt')
|
||||
expect(getDefaultGroupName('DeepSeek Chat', provider)).toBe('deepseek')
|
||||
expect(getDefaultGroupName('foo:bar', provider)).toBe('foo')
|
||||
})
|
||||
})
|
||||
|
||||
it('should use first and second delimiters for default providers', () => {
|
||||
// 默认情况下,'/', ' ', ':' 属于第一类分隔符,'-' '_' 属于第二类
|
||||
expect(getDefaultGroupName('Qwen/Qwen3-32B', 'foobar')).toBe('qwen')
|
||||
expect(getDefaultGroupName('gpt-4.1-mini', 'foobar')).toBe('gpt-4.1')
|
||||
expect(getDefaultGroupName('gpt-4.1', 'foobar')).toBe('gpt-4.1')
|
||||
expect(getDefaultGroupName('DeepSeek Chat', 'foobar')).toBe('deepseek')
|
||||
expect(getDefaultGroupName('foo:bar', 'foobar')).toBe('foo')
|
||||
})
|
||||
|
||||
it('should fallback to id if no delimiters', () => {
|
||||
// 没有分隔符时返回 id
|
||||
const specialProviders = ['aihubmix', 'silicon', 'ocoolai', 'o3', 'dmxapi']
|
||||
specialProviders.forEach((provider) => {
|
||||
expect(getDefaultGroupName('o3', provider)).toBe('o3')
|
||||
})
|
||||
expect(getDefaultGroupName('o3', 'openai')).toBe('o3')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,26 +1,49 @@
|
||||
/**
|
||||
* 从模型 ID 中提取默认组名。
|
||||
* 规则如下:
|
||||
* 1. 第一类分隔规则:以第一个出现的分隔符分割,取第 0 个部分作为组名。
|
||||
* 2. 第二类分隔规则:取前两个部分拼接(如 'a-b-c' 得到 'a-b')。
|
||||
* 3. 其他情况返回 id。
|
||||
*
|
||||
* 例如:
|
||||
* - 'gpt-3.5-turbo-16k-0613' 转换为 'GPT-3.5-Turbo'
|
||||
* - 'qwen2:1.5b' 转换为 'QWEN2'。
|
||||
* - 'gpt-3.5-turbo-16k-0613' => 'gpt-3.5'
|
||||
* - 'qwen3:32b' => 'qwen3'
|
||||
* - 'Qwen/Qwen3-32b' => 'qwen'
|
||||
* - 'deepseek-r1' => 'deepseek-r1'
|
||||
* - 'o3' => 'o3'
|
||||
*
|
||||
* @param id 模型 ID 字符串
|
||||
* @param provider 提供商 ID 字符串
|
||||
* @returns string 提取的组名
|
||||
*/
|
||||
export const getDefaultGroupName = (id: string) => {
|
||||
if (id.includes('/')) {
|
||||
return id.split('/')[0]
|
||||
export const getDefaultGroupName = (id: string, provider?: string) => {
|
||||
const str = id.toLowerCase()
|
||||
|
||||
// 定义分隔符
|
||||
let firstDelimiters = ['/', ' ', ':']
|
||||
let secondDelimiters = ['-', '_']
|
||||
|
||||
if (provider && ['aihubmix', 'silicon', 'ocoolai', 'o3', 'dmxapi'].includes(provider.toLowerCase())) {
|
||||
firstDelimiters = ['/', ' ', '-', '_', ':']
|
||||
secondDelimiters = []
|
||||
}
|
||||
|
||||
if (id.includes(':')) {
|
||||
return id.split(':')[0]
|
||||
// 第一类分隔规则
|
||||
for (const delimiter of firstDelimiters) {
|
||||
if (str.includes(delimiter)) {
|
||||
return str.split(delimiter)[0]
|
||||
}
|
||||
}
|
||||
|
||||
if (id.includes('-')) {
|
||||
const parts = id.split('-')
|
||||
return parts[0] + '-' + parts[1]
|
||||
// 第二类分隔规则
|
||||
for (const delimiter of secondDelimiters) {
|
||||
if (str.includes(delimiter)) {
|
||||
const parts = str.split(delimiter)
|
||||
return parts.length > 1 ? parts[0] + '-' + parts[1] : parts[0]
|
||||
}
|
||||
}
|
||||
|
||||
return id
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user