diff --git a/model/model_meta.go b/model/model_meta.go index 3598cb7c..4faf7a84 100644 --- a/model/model_meta.go +++ b/model/model_meta.go @@ -3,6 +3,7 @@ package model import ( "one-api/common" "strconv" + "strings" "gorm.io/gorm" ) @@ -20,6 +21,14 @@ import ( // 3. 不存在传递依赖(描述、标签等都依赖于 ModelName,而非依赖于其他非主键列) // 这样既保证了数据一致性,也方便后期扩展 +// 模型名称匹配规则 +const ( + NameRuleExact = iota // 0 精确匹配 + NameRulePrefix // 1 前缀匹配 + NameRuleContains // 2 包含匹配 + NameRuleSuffix // 3 后缀匹配 +) + type BoundChannel struct { Name string `json:"name"` Type int `json:"type"` @@ -40,6 +49,7 @@ type Model struct { BoundChannels []BoundChannel `json:"bound_channels,omitempty" gorm:"-"` EnableGroups []string `json:"enable_groups,omitempty" gorm:"-"` QuotaType int `json:"quota_type" gorm:"-"` + NameRule int `json:"name_rule" gorm:"default:0"` } // Insert 创建新的模型元数据记录 @@ -93,6 +103,52 @@ func GetBoundChannels(modelName string) ([]BoundChannel, error) { 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 根据关键词和供应商搜索模型,支持分页 func SearchModels(keyword string, vendor string, offset int, limit int) ([]*Model, int64, error) { var models []*Model diff --git a/web/src/components/table/models/ModelsColumnDefs.js b/web/src/components/table/models/ModelsColumnDefs.js index f71686fc..c02201c4 100644 --- a/web/src/components/table/models/ModelsColumnDefs.js +++ b/web/src/components/table/models/ModelsColumnDefs.js @@ -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 ( + + {cfg.label} + + ); +}; + export const getModelsColumns = ({ t, manageModel, @@ -202,6 +219,11 @@ export const getModelsColumns = ({ ), }, + { + title: t('匹配类型'), + dataIndex: 'name_rule', + render: (val) => renderNameRule(val, t), + }, { title: t('描述'), dataIndex: 'description', diff --git a/web/src/components/table/models/modals/EditModelModal.jsx b/web/src/components/table/models/modals/EditModelModal.jsx index 1a1c9787..bc22d006 100644 --- a/web/src/components/table/models/modals/EditModelModal.jsx +++ b/web/src/components/table/models/modals/EditModelModal.jsx @@ -40,6 +40,13 @@ import { API, showError, showSuccess } from '../../../../helpers'; import { useTranslation } from 'react-i18next'; import { useIsMobile } from '../../../../hooks/common/useIsMobile'; +const nameRuleOptions = [ + { label: '精确名称匹配', value: 0 }, + { label: '前缀名称匹配', value: 1 }, + { label: '包含名称匹配', value: 2 }, + { label: '后缀名称匹配', value: 3 }, +]; + const endpointOptions = [ { label: 'OpenAI', value: 'openai' }, { label: 'Anthropic', value: 'anthropic' }, @@ -111,6 +118,7 @@ const EditModelModal = (props) => { vendor: '', vendor_icon: '', endpoints: [], + name_rule: props.editingModel?.model_name ? 0 : undefined, // 通过未配置模型过来的固定为精确匹配 status: true, }); @@ -301,6 +309,20 @@ const EditModelModal = (props) => { showClear /> + + + ({ label: t(o.label), value: o.value }))} + rules={[{ required: true, message: t('请选择名称匹配类型') }]} + disabled={!!props.editingModel?.model_name} // 通过未配置模型过来的禁用选择 + style={{ width: '100%' }} + extraText={t('根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含')} + /> + +