From fc69f4f757fef12b6fe878a2668524910741fc3e Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Mon, 4 Aug 2025 16:01:56 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20model=20name=20matchi?= =?UTF-8?q?ng=20rules=20with=20priority-based=20lookup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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] --- model/model_meta.go | 56 +++++++++++++++++++ .../table/models/ModelsColumnDefs.js | 22 ++++++++ .../table/models/modals/EditModelModal.jsx | 22 ++++++++ 3 files changed, 100 insertions(+) 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('根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含')} + /> + +