From 70560d53718f9e8ed2c70d15bc26b043c9b562b0 Mon Sep 17 00:00:00 2001 From: Clansty Date: Sun, 29 Mar 2026 02:22:24 +0800 Subject: [PATCH 1/3] feat: add IncludeModelName option to channel affinity rules for per-model affinity tracking --- service/channel_affinity.go | 21 +++++++++++++++---- service/channel_affinity_template_test.go | 2 +- .../channel_affinity_setting.go | 1 + 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/service/channel_affinity.go b/service/channel_affinity.go index 9f89585f..e09cb01f 100644 --- a/service/channel_affinity.go +++ b/service/channel_affinity.go @@ -166,12 +166,22 @@ func GetChannelAffinityCacheStats() ChannelAffinityCacheStats { unknown++ continue } - if rule.IncludeUsingGroup { + if rule.IncludeModelName { if len(parts) < 3 { unknown++ continue } } + if rule.IncludeUsingGroup { + minParts := 3 + if rule.IncludeModelName { + minParts = 4 + } + if len(parts) < minParts { + unknown++ + continue + } + } byRuleName[ruleName]++ } @@ -319,11 +329,14 @@ func extractChannelAffinityValue(c *gin.Context, src operation_setting.ChannelAf } } -func buildChannelAffinityCacheKeySuffix(rule operation_setting.ChannelAffinityRule, usingGroup string, affinityValue string) string { - parts := make([]string, 0, 3) +func buildChannelAffinityCacheKeySuffix(rule operation_setting.ChannelAffinityRule, modelName string, usingGroup string, affinityValue string) string { + parts := make([]string, 0, 4) if rule.IncludeRuleName && rule.Name != "" { parts = append(parts, rule.Name) } + if rule.IncludeModelName && modelName != "" { + parts = append(parts, modelName) + } if rule.IncludeUsingGroup && usingGroup != "" { parts = append(parts, usingGroup) } @@ -573,7 +586,7 @@ func GetPreferredChannelByAffinity(c *gin.Context, modelName string, usingGroup if ttlSeconds <= 0 { ttlSeconds = setting.DefaultTTLSeconds } - cacheKeySuffix := buildChannelAffinityCacheKeySuffix(rule, usingGroup, affinityValue) + cacheKeySuffix := buildChannelAffinityCacheKeySuffix(rule, modelName, usingGroup, affinityValue) cacheKeyFull := channelAffinityCacheNamespace + ":" + cacheKeySuffix setChannelAffinityContext(c, channelAffinityMeta{ CacheKey: cacheKeyFull, diff --git a/service/channel_affinity_template_test.go b/service/channel_affinity_template_test.go index 264f9122..033cbd83 100644 --- a/service/channel_affinity_template_test.go +++ b/service/channel_affinity_template_test.go @@ -193,7 +193,7 @@ func TestChannelAffinityHitCodexTemplatePassHeadersEffective(t *testing.T) { require.NotNil(t, codexRule) affinityValue := fmt.Sprintf("pc-hit-%d", time.Now().UnixNano()) - cacheKeySuffix := buildChannelAffinityCacheKeySuffix(*codexRule, "default", affinityValue) + cacheKeySuffix := buildChannelAffinityCacheKeySuffix(*codexRule, "gpt-5", "default", affinityValue) cache := getChannelAffinityCache() require.NoError(t, cache.SetWithTTL(cacheKeySuffix, 9527, time.Minute)) diff --git a/setting/operation_setting/channel_affinity_setting.go b/setting/operation_setting/channel_affinity_setting.go index 74213e99..ebe00f44 100644 --- a/setting/operation_setting/channel_affinity_setting.go +++ b/setting/operation_setting/channel_affinity_setting.go @@ -23,6 +23,7 @@ type ChannelAffinityRule struct { SkipRetryOnFailure bool `json:"skip_retry_on_failure,omitempty"` IncludeUsingGroup bool `json:"include_using_group"` + IncludeModelName bool `json:"include_model_name"` IncludeRuleName bool `json:"include_rule_name"` } From 116e0b8f1ce0740b486e20a1734c93ba4943bb25 Mon Sep 17 00:00:00 2001 From: Clansty Date: Sun, 29 Mar 2026 02:48:37 +0800 Subject: [PATCH 2/3] feat: add include_model_name UI switch to channel affinity settings --- web/src/i18n/locales/en.json | 2 ++ web/src/i18n/locales/fr.json | 2 ++ web/src/i18n/locales/ja.json | 2 ++ web/src/i18n/locales/ru.json | 2 ++ web/src/i18n/locales/vi.json | 2 ++ .../Operation/SettingsChannelAffinity.jsx | 20 +++++++++++++++++-- 6 files changed, 28 insertions(+), 2 deletions(-) diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index e392379e..6a147021 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -401,6 +401,8 @@ "作废后该订阅将立即失效,历史记录不受影响。是否继续?": "After invalidation, the subscription becomes invalid immediately. History is not affected. Continue?", "作用域": "Scope", "作用域:包含分组": "Scope: Include Group", + "作用域:包含模型名称": "Scope: Include Model Name", + "开启后,模型名称会参与 cache key(不同模型隔离)。": "When enabled, the model name is included in the cache key (isolates different models).", "作用域:包含规则名称": "Scope: Include Rule Name", "你似乎并没有修改什么": "You seem to have not modified anything", "你可以在“自定义模型名称”处手动添加它们,然后点击填入后再提交,或者直接使用下方操作自动处理。": "You can manually add them under “Custom model names”, click Fill and submit, or use the actions below to handle them automatically.", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index 6ff22ab5..2ebefb71 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -397,6 +397,8 @@ "作废后该订阅将立即失效,历史记录不受影响。是否继续?": "Après invalidation, l'abonnement devient immédiatement invalide. L'historique n'est pas affecté. Continuer ?", "作用域": "Portée", "作用域:包含分组": "Portée : inclure le groupe", + "作用域:包含模型名称": "Portée : inclure le nom du modèle", + "开启后,模型名称会参与 cache key(不同模型隔离)。": "Lorsque activé, le nom du modèle est inclus dans la clé de cache (isole les différents modèles).", "作用域:包含规则名称": "Portée : inclure le nom de la règle", "你似乎并没有修改什么": "Vous ne semblez rien avoir modifié", "你可以在“自定义模型名称”处手动添加它们,然后点击填入后再提交,或者直接使用下方操作自动处理。": "Vous pouvez les ajouter manuellement dans « Noms de modèles personnalisés », cliquer sur Remplir puis soumettre, ou utiliser directement les actions ci-dessous pour les traiter automatiquement.", diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index b2a59fc4..9a8a7ac6 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -393,6 +393,8 @@ "作废后该订阅将立即失效,历史记录不受影响。是否继续?": "無効化するとこのサブスクリプションは直ちに失効します。履歴には影響しません。続行しますか?", "作用域": "スコープ", "作用域:包含分组": "スコープ:グループを含む", + "作用域:包含模型名称": "スコープ:モデル名を含む", + "开启后,模型名称会参与 cache key(不同模型隔离)。": "有効にすると、モデル名がキャッシュキーに含まれます(異なるモデルを分離)。", "作用域:包含规则名称": "スコープ:ルール名を含む", "你似乎并没有修改什么": "何も変更されていないようです", "你可以在“自定义模型名称”处手动添加它们,然后点击填入后再提交,或者直接使用下方操作自动处理。": "You can manually add them under “Custom model names”, click Fill and submit, or use the actions below to handle them automatically.", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index ccf10242..9368d630 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -400,6 +400,8 @@ "作废后该订阅将立即失效,历史记录不受影响。是否继续?": "После аннулирования подписка сразу станет недействительной. История не изменится. Продолжить?", "作用域": "Область действия", "作用域:包含分组": "Область действия: включить группу", + "作用域:包含模型名称": "Область действия: включить имя модели", + "开启后,模型名称会参与 cache key(不同模型隔离)。": "При включении имя модели включается в ключ кэша (изолирует разные модели).", "作用域:包含规则名称": "Область действия: включить имя правила", "你似乎并没有修改什么": "Похоже, вы ничего не изменили", "你可以在“自定义模型名称”处手动添加它们,然后点击填入后再提交,或者直接使用下方操作自动处理。": "Вы можете добавить их вручную в разделе «Пользовательские названия моделей», нажать «Заполнить», затем отправить или воспользоваться действиями ниже для автоматической обработки.", diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index 5c9e1e8c..1a5be4d7 100644 --- a/web/src/i18n/locales/vi.json +++ b/web/src/i18n/locales/vi.json @@ -394,6 +394,8 @@ "作废后该订阅将立即失效,历史记录不受影响。是否继续?": "Sau khi vô hiệu, đăng ký sẽ mất hiệu lực ngay. Lịch sử không bị ảnh hưởng. Tiếp tục?", "作用域": "Phạm vi", "作用域:包含分组": "Phạm vi: Bao gồm nhóm", + "作用域:包含模型名称": "Phạm vi: Bao gồm tên mô hình", + "开启后,模型名称会参与 cache key(不同模型隔离)。": "Khi bật, tên mô hình sẽ được bao gồm trong cache key (cách ly các mô hình khác nhau).", "作用域:包含规则名称": "Phạm vi: Bao gồm tên quy tắc", "你似乎并没有修改什么": "Bạn dường như không sửa đổi gì cả", "你可以在“自定义模型名称”处手动添加它们,然后点击填入后再提交,或者直接使用下方操作自动处理。": "You can manually add them under “Custom model names”, click Fill and submit, or use the actions below to handle them automatically.", diff --git a/web/src/pages/Setting/Operation/SettingsChannelAffinity.jsx b/web/src/pages/Setting/Operation/SettingsChannelAffinity.jsx index c179e855..192dc291 100644 --- a/web/src/pages/Setting/Operation/SettingsChannelAffinity.jsx +++ b/web/src/pages/Setting/Operation/SettingsChannelAffinity.jsx @@ -103,6 +103,7 @@ const RULES_JSON_PLACEHOLDER = `[ }, "skip_retry_on_failure": false, "include_using_group": true, + "include_model_name": false, "include_rule_name": true } ]`; @@ -246,6 +247,7 @@ export default function SettingsChannelAffinity(props) { ttl_seconds: Number(r.ttl_seconds || 0), skip_retry_on_failure: !!r.skip_retry_on_failure, include_using_group: r.include_using_group ?? true, + include_model_name: !!r.include_model_name, include_rule_name: r.include_rule_name ?? true, param_override_template_json: r.param_override_template ? stringifyPretty(r.param_override_template) @@ -582,6 +584,7 @@ export default function SettingsChannelAffinity(props) { render: (_, record) => { const tags = []; if (record?.include_using_group) tags.push('分组'); + if (record?.include_model_name) tags.push('模型'); if (record?.include_rule_name) tags.push('规则'); if (tags.length === 0) return '-'; return tags.map((x) => ( @@ -650,6 +653,7 @@ export default function SettingsChannelAffinity(props) { ttl_seconds: 0, skip_retry_on_failure: false, include_using_group: true, + include_model_name: false, include_rule_name: true, }; setEditingRule(nextRule); @@ -721,6 +725,7 @@ export default function SettingsChannelAffinity(props) { value_regex: (values.value_regex || '').trim(), ttl_seconds: Number(values.ttl_seconds || 0), include_using_group: !!values.include_using_group, + include_model_name: !!values.include_model_name, include_rule_name: !!values.include_rule_name, ...(values.skip_retry_on_failure ? { skip_retry_on_failure: true } @@ -1251,7 +1256,7 @@ export default function SettingsChannelAffinity(props) { - + - + + + + {t( + '开启后,模型名称会参与 cache key(不同模型隔离)。', + )} + + + Date: Tue, 7 Apr 2026 20:00:34 +0800 Subject: [PATCH 3/3] fix: wrap scope tag labels with t() for i18n support --- web/src/pages/Setting/Operation/SettingsChannelAffinity.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/pages/Setting/Operation/SettingsChannelAffinity.jsx b/web/src/pages/Setting/Operation/SettingsChannelAffinity.jsx index 192dc291..7d3cdd6e 100644 --- a/web/src/pages/Setting/Operation/SettingsChannelAffinity.jsx +++ b/web/src/pages/Setting/Operation/SettingsChannelAffinity.jsx @@ -583,9 +583,9 @@ export default function SettingsChannelAffinity(props) { title: t('作用域'), render: (_, record) => { const tags = []; - if (record?.include_using_group) tags.push('分组'); - if (record?.include_model_name) tags.push('模型'); - if (record?.include_rule_name) tags.push('规则'); + if (record?.include_using_group) tags.push(t('分组')); + if (record?.include_model_name) tags.push(t('模型')); + if (record?.include_rule_name) tags.push(t('规则')); if (tags.length === 0) return '-'; return tags.map((x) => (