✨ feat: add model name matching rules with priority-based lookup
Add flexible model name matching system to support different matching patterns: Backend changes: - Add `name_rule` field to Model struct with 4 matching types: * 0: Exact match (default) * 1: Prefix match * 2: Contains match * 3: Suffix match - Implement `FindModelByNameWithRule` function with priority order: exact > prefix > suffix > contains - Add database migration for new `name_rule` column Frontend changes: - Add "Match Type" column in models table with colored tags - Add name rule selector in create/edit modal with validation - Auto-set exact match and disable selection for preconfigured models - Add explanatory text showing priority order - Support i18n for all new UI elements This enables users to define model patterns once and reuse configurations across similar models, reducing repetitive setup while maintaining exact match priority for specific overrides. Closes: #[issue-number]
This commit is contained in:
@@ -3,6 +3,7 @@ package model
|
|||||||
import (
|
import (
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -20,6 +21,14 @@ import (
|
|||||||
// 3. 不存在传递依赖(描述、标签等都依赖于 ModelName,而非依赖于其他非主键列)
|
// 3. 不存在传递依赖(描述、标签等都依赖于 ModelName,而非依赖于其他非主键列)
|
||||||
// 这样既保证了数据一致性,也方便后期扩展
|
// 这样既保证了数据一致性,也方便后期扩展
|
||||||
|
|
||||||
|
// 模型名称匹配规则
|
||||||
|
const (
|
||||||
|
NameRuleExact = iota // 0 精确匹配
|
||||||
|
NameRulePrefix // 1 前缀匹配
|
||||||
|
NameRuleContains // 2 包含匹配
|
||||||
|
NameRuleSuffix // 3 后缀匹配
|
||||||
|
)
|
||||||
|
|
||||||
type BoundChannel struct {
|
type BoundChannel struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type int `json:"type"`
|
Type int `json:"type"`
|
||||||
@@ -40,6 +49,7 @@ type Model struct {
|
|||||||
BoundChannels []BoundChannel `json:"bound_channels,omitempty" gorm:"-"`
|
BoundChannels []BoundChannel `json:"bound_channels,omitempty" gorm:"-"`
|
||||||
EnableGroups []string `json:"enable_groups,omitempty" gorm:"-"`
|
EnableGroups []string `json:"enable_groups,omitempty" gorm:"-"`
|
||||||
QuotaType int `json:"quota_type" gorm:"-"`
|
QuotaType int `json:"quota_type" gorm:"-"`
|
||||||
|
NameRule int `json:"name_rule" gorm:"default:0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert 创建新的模型元数据记录
|
// Insert 创建新的模型元数据记录
|
||||||
@@ -93,6 +103,52 @@ func GetBoundChannels(modelName string) ([]BoundChannel, error) {
|
|||||||
return channels, err
|
return channels, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindModelByNameWithRule 根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含
|
||||||
|
func FindModelByNameWithRule(name string) (*Model, error) {
|
||||||
|
// 1. 精确匹配
|
||||||
|
if m, err := GetModelByName(name); err == nil {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
// 2. 规则匹配
|
||||||
|
var models []*Model
|
||||||
|
if err := DB.Where("name_rule <> ?", NameRuleExact).Find(&models).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var prefixMatch, suffixMatch, containsMatch *Model
|
||||||
|
for _, m := range models {
|
||||||
|
switch m.NameRule {
|
||||||
|
case NameRulePrefix:
|
||||||
|
if strings.HasPrefix(name, m.ModelName) {
|
||||||
|
if prefixMatch == nil || len(m.ModelName) > len(prefixMatch.ModelName) {
|
||||||
|
prefixMatch = m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case NameRuleSuffix:
|
||||||
|
if strings.HasSuffix(name, m.ModelName) {
|
||||||
|
if suffixMatch == nil || len(m.ModelName) > len(suffixMatch.ModelName) {
|
||||||
|
suffixMatch = m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case NameRuleContains:
|
||||||
|
if strings.Contains(name, m.ModelName) {
|
||||||
|
if containsMatch == nil || len(m.ModelName) > len(containsMatch.ModelName) {
|
||||||
|
containsMatch = m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if prefixMatch != nil {
|
||||||
|
return prefixMatch, nil
|
||||||
|
}
|
||||||
|
if suffixMatch != nil {
|
||||||
|
return suffixMatch, nil
|
||||||
|
}
|
||||||
|
if containsMatch != nil {
|
||||||
|
return containsMatch, nil
|
||||||
|
}
|
||||||
|
return nil, gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
// SearchModels 根据关键词和供应商搜索模型,支持分页
|
// SearchModels 根据关键词和供应商搜索模型,支持分页
|
||||||
func SearchModels(keyword string, vendor string, offset int, limit int) ([]*Model, int64, error) {
|
func SearchModels(keyword string, vendor string, offset int, limit int) ([]*Model, int64, error) {
|
||||||
var models []*Model
|
var models []*Model
|
||||||
|
|||||||
@@ -184,6 +184,23 @@ const renderOperations = (text, record, setEditingModel, setShowEdit, manageMode
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 名称匹配类型渲染
|
||||||
|
const renderNameRule = (rule, t) => {
|
||||||
|
const map = {
|
||||||
|
0: { color: 'green', label: t('精确') },
|
||||||
|
1: { color: 'blue', label: t('前缀') },
|
||||||
|
2: { color: 'orange', label: t('包含') },
|
||||||
|
3: { color: 'purple', label: t('后缀') },
|
||||||
|
};
|
||||||
|
const cfg = map[rule];
|
||||||
|
if (!cfg) return '-';
|
||||||
|
return (
|
||||||
|
<Tag color={cfg.color} size="small" shape='circle'>
|
||||||
|
{cfg.label}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const getModelsColumns = ({
|
export const getModelsColumns = ({
|
||||||
t,
|
t,
|
||||||
manageModel,
|
manageModel,
|
||||||
@@ -202,6 +219,11 @@ export const getModelsColumns = ({
|
|||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t('匹配类型'),
|
||||||
|
dataIndex: 'name_rule',
|
||||||
|
render: (val) => renderNameRule(val, t),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t('描述'),
|
title: t('描述'),
|
||||||
dataIndex: 'description',
|
dataIndex: 'description',
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ import { API, showError, showSuccess } from '../../../../helpers';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
||||||
|
|
||||||
|
const nameRuleOptions = [
|
||||||
|
{ label: '精确名称匹配', value: 0 },
|
||||||
|
{ label: '前缀名称匹配', value: 1 },
|
||||||
|
{ label: '包含名称匹配', value: 2 },
|
||||||
|
{ label: '后缀名称匹配', value: 3 },
|
||||||
|
];
|
||||||
|
|
||||||
const endpointOptions = [
|
const endpointOptions = [
|
||||||
{ label: 'OpenAI', value: 'openai' },
|
{ label: 'OpenAI', value: 'openai' },
|
||||||
{ label: 'Anthropic', value: 'anthropic' },
|
{ label: 'Anthropic', value: 'anthropic' },
|
||||||
@@ -111,6 +118,7 @@ const EditModelModal = (props) => {
|
|||||||
vendor: '',
|
vendor: '',
|
||||||
vendor_icon: '',
|
vendor_icon: '',
|
||||||
endpoints: [],
|
endpoints: [],
|
||||||
|
name_rule: props.editingModel?.model_name ? 0 : undefined, // 通过未配置模型过来的固定为精确匹配
|
||||||
status: true,
|
status: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -301,6 +309,20 @@ const EditModelModal = (props) => {
|
|||||||
showClear
|
showClear
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
|
<Col span={24}>
|
||||||
|
<Form.Select
|
||||||
|
field='name_rule'
|
||||||
|
label={t('名称匹配类型')}
|
||||||
|
placeholder={t('请选择名称匹配类型')}
|
||||||
|
optionList={nameRuleOptions.map(o => ({ label: t(o.label), value: o.value }))}
|
||||||
|
rules={[{ required: true, message: t('请选择名称匹配类型') }]}
|
||||||
|
disabled={!!props.editingModel?.model_name} // 通过未配置模型过来的禁用选择
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
extraText={t('根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含')}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
field='description'
|
field='description'
|
||||||
|
|||||||
Reference in New Issue
Block a user