diff --git a/middleware/distributor.go b/middleware/distributor.go
index 34865c0ce..258aebb57 100644
--- a/middleware/distributor.go
+++ b/middleware/distributor.go
@@ -102,14 +102,10 @@ func Distribute() func(c *gin.Context) {
}
if preferredChannelID, found := service.GetPreferredChannelByAffinity(c, modelRequest.Model, usingGroup); found {
+ affinityUsable := false
preferred, err := model.CacheGetChannel(preferredChannelID)
- if err == nil && preferred != nil {
- if preferred.Status != common.ChannelStatusEnabled {
- if service.ShouldSkipRetryAfterChannelAffinityFailure(c) {
- abortWithOpenAiMessage(c, http.StatusForbidden, i18n.T(c, i18n.MsgDistributorAffinityChannelDisabled))
- return
- }
- } else if usingGroup == "auto" {
+ if err == nil && preferred != nil && preferred.Status == common.ChannelStatusEnabled {
+ if usingGroup == "auto" {
userGroup := common.GetContextKeyString(c, constant.ContextKeyUserGroup)
autoGroups := service.GetUserAutoGroup(userGroup)
for _, g := range autoGroups {
@@ -117,6 +113,7 @@ func Distribute() func(c *gin.Context) {
selectGroup = g
common.SetContextKey(c, constant.ContextKeyAutoGroup, g)
channel = preferred
+ affinityUsable = true
service.MarkChannelAffinityUsed(c, g, preferred.Id)
break
}
@@ -124,9 +121,13 @@ func Distribute() func(c *gin.Context) {
} else if model.IsChannelEnabledForGroupModel(usingGroup, modelRequest.Model, preferred.Id) {
channel = preferred
selectGroup = usingGroup
+ affinityUsable = true
service.MarkChannelAffinityUsed(c, usingGroup, preferred.Id)
}
}
+ if !affinityUsable && !service.ShouldKeepChannelAffinityOnChannelDisabled() {
+ service.ClearCurrentChannelAffinityCache(c)
+ }
}
if channel == nil {
diff --git a/service/channel_affinity.go b/service/channel_affinity.go
index f16c350bb..96ec13e24 100644
--- a/service/channel_affinity.go
+++ b/service/channel_affinity.go
@@ -641,6 +641,38 @@ func ShouldSkipRetryAfterChannelAffinityFailure(c *gin.Context) bool {
return meta.SkipRetry
}
+func ClearCurrentChannelAffinityCache(c *gin.Context) bool {
+ if c == nil {
+ return false
+ }
+ cacheKey, _, ok := getChannelAffinityContext(c)
+ if !ok || cacheKey == "" {
+ return false
+ }
+
+ cache := getChannelAffinityCache()
+ deleted, err := cache.DeleteMany([]string{cacheKey})
+ if err != nil {
+ common.SysError(fmt.Sprintf("channel affinity cache delete current failed: err=%v", err))
+ return false
+ }
+ c.Set(ginKeyChannelAffinitySkipRetry, false)
+ for _, ok := range deleted {
+ if ok {
+ return true
+ }
+ }
+ return false
+}
+
+func ShouldKeepChannelAffinityOnChannelDisabled() bool {
+ setting := operation_setting.GetChannelAffinitySetting()
+ if setting == nil {
+ return false
+ }
+ return setting.KeepOnChannelDisabled
+}
+
func MarkChannelAffinityUsed(c *gin.Context, selectedGroup string, channelID int) {
if c == nil || channelID <= 0 {
return
diff --git a/service/channel_affinity_template_test.go b/service/channel_affinity_template_test.go
index 91844fc33..fb703a24e 100644
--- a/service/channel_affinity_template_test.go
+++ b/service/channel_affinity_template_test.go
@@ -236,6 +236,33 @@ func TestGetPreferredChannelByAffinity_RequestHeaderKeySource(t *testing.T) {
require.Equal(t, buildChannelAffinityKeyHint(affinityValue), meta.KeyHint)
}
+func TestClearCurrentChannelAffinityCache(t *testing.T) {
+ gin.SetMode(gin.TestMode)
+
+ cacheKeySuffix := fmt.Sprintf("codex cli trace:default:clear-current-%d", time.Now().UnixNano())
+ cacheKeyFull := channelAffinityCacheNamespace + ":" + cacheKeySuffix
+ cache := getChannelAffinityCache()
+ require.NoError(t, cache.SetWithTTL(cacheKeySuffix, 9527, time.Minute))
+ t.Cleanup(func() {
+ _, _ = cache.DeleteMany([]string{cacheKeySuffix})
+ })
+
+ ctx := buildChannelAffinityTemplateContextForTest(channelAffinityMeta{
+ CacheKey: cacheKeyFull,
+ TTLSeconds: 60,
+ RuleName: "codex cli trace",
+ SkipRetry: true,
+ })
+ require.True(t, ShouldSkipRetryAfterChannelAffinityFailure(ctx))
+
+ deleted := ClearCurrentChannelAffinityCache(ctx)
+ require.True(t, deleted)
+ _, found, err := cache.Get(cacheKeySuffix)
+ require.NoError(t, err)
+ require.False(t, found)
+ require.False(t, ShouldSkipRetryAfterChannelAffinityFailure(ctx))
+}
+
func TestChannelAffinityHitCodexTemplatePassHeadersEffective(t *testing.T) {
gin.SetMode(gin.TestMode)
diff --git a/setting/operation_setting/channel_affinity_setting.go b/setting/operation_setting/channel_affinity_setting.go
index bd573696d..a61d25468 100644
--- a/setting/operation_setting/channel_affinity_setting.go
+++ b/setting/operation_setting/channel_affinity_setting.go
@@ -28,11 +28,12 @@ type ChannelAffinityRule struct {
}
type ChannelAffinitySetting struct {
- Enabled bool `json:"enabled"`
- SwitchOnSuccess bool `json:"switch_on_success"`
- MaxEntries int `json:"max_entries"`
- DefaultTTLSeconds int `json:"default_ttl_seconds"`
- Rules []ChannelAffinityRule `json:"rules"`
+ Enabled bool `json:"enabled"`
+ SwitchOnSuccess bool `json:"switch_on_success"`
+ KeepOnChannelDisabled bool `json:"keep_on_channel_disabled"`
+ MaxEntries int `json:"max_entries"`
+ DefaultTTLSeconds int `json:"default_ttl_seconds"`
+ Rules []ChannelAffinityRule `json:"rules"`
}
var codexCliPassThroughHeaders = []string{
@@ -74,10 +75,11 @@ func buildPassHeaderTemplate(headers []string) map[string]interface{} {
}
var channelAffinitySetting = ChannelAffinitySetting{
- Enabled: true,
- SwitchOnSuccess: true,
- MaxEntries: 100_000,
- DefaultTTLSeconds: 3600,
+ Enabled: true,
+ SwitchOnSuccess: true,
+ KeepOnChannelDisabled: false,
+ MaxEntries: 100_000,
+ DefaultTTLSeconds: 3600,
Rules: []ChannelAffinityRule{
{
Name: "codex cli trace",
diff --git a/web/classic/src/i18n/locales/en.json b/web/classic/src/i18n/locales/en.json
index 17511d2a5..a63ab5097 100644
--- a/web/classic/src/i18n/locales/en.json
+++ b/web/classic/src/i18n/locales/en.json
@@ -1197,6 +1197,7 @@
"套餐的基本信息和定价": "Basic plan info and pricing",
"如:大带宽批量分析图片推荐": "e.g. Large bandwidth batch analysis of image recommendations",
"如:香港线路": "e.g. Hong Kong line",
+ "开启后,亲和到的渠道被禁用,或不再适用于当前分组/模型时,仍保留这条亲和;关闭时会删除并重新选择渠道。": "When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. When disabled, the entry will be deleted and another channel will be selected.",
"如果亲和到的渠道失败,重试到其他渠道成功后,将亲和更新到成功的渠道。": "If the affinity channel fails, after a successful retry on another channel, the affinity will be updated to the successful channel.",
"如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "If you are connecting to upstream One API or New API forwarding projects, please use OpenAI type. Do not use this type unless you know what you are doing.",
"如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面": "If the user request contains a system prompt, this setting will be appended to the user's system prompt",
@@ -1579,6 +1580,7 @@
"成功": "Success",
"成功兑换额度:": "Successful redemption amount:",
"成功后切换亲和": "Switch Affinity on Success",
+ "渠道禁用后保留亲和": "Keep Affinity When Channel Is Disabled",
"成功时自动启用通道": "Enable channel when successful",
"我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销": "I have understood that disabling two-factor authentication will permanently delete all related settings and backup codes, this operation cannot be undone",
"我已阅读并同意": "I have read and agree to",
diff --git a/web/classic/src/i18n/locales/fr.json b/web/classic/src/i18n/locales/fr.json
index a24d32bad..f8d92677e 100644
--- a/web/classic/src/i18n/locales/fr.json
+++ b/web/classic/src/i18n/locales/fr.json
@@ -1193,6 +1193,7 @@
"套餐的基本信息和定价": "Informations de base et tarification du plan",
"如:大带宽批量分析图片推荐": "par exemple, Recommandations d'analyse d'images par lots à large bande passante",
"如:香港线路": "par exemple, Ligne de Hong Kong",
+ "开启后,亲和到的渠道被禁用,或不再适用于当前分组/模型时,仍保留这条亲和;关闭时会删除并重新选择渠道。": "Lorsque cette option est activée, conserver l'entrée d'affinité même si le canal d'affinité est désactivé ou n'est plus utilisable pour le groupe/modèle actuel. Lorsqu'elle est désactivée, l'entrée sera supprimée et un autre canal sera sélectionné.",
"如果亲和到的渠道失败,重试到其他渠道成功后,将亲和更新到成功的渠道。": "Si le canal d'affinité échoue, après une nouvelle tentative réussie sur un autre canal, l'affinité sera mise à jour vers le canal réussi.",
"如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "Si vous vous connectez à des projets de redirection One API ou New API en amont, veuillez utiliser le type OpenAI. N'utilisez pas ce type, sauf si vous savez ce que vous faites.",
"如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面": "Si la requête de l'utilisateur contient un prompt système, utilisez ce paramètre pour le concaténer avant le prompt système de l'utilisateur",
@@ -1584,6 +1585,7 @@
"成功": "Succès",
"成功兑换额度:": "Montant de l'échange réussi :",
"成功后切换亲和": "Changer l'affinité en cas de succès",
+ "渠道禁用后保留亲和": "Conserver l'affinité lorsque le canal est désactivé",
"成功时自动启用通道": "Activer le canal en cas de succès",
"我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销": "J'ai compris que la désactivation de l'authentification à deux facteurs supprimera définitivement tous les paramètres et codes de sauvegarde associés, cette opération ne peut pas être annulée",
"我已阅读并同意": "J'ai lu et j'accepte",
diff --git a/web/classic/src/i18n/locales/ja.json b/web/classic/src/i18n/locales/ja.json
index dde2a1a57..1fceb066f 100644
--- a/web/classic/src/i18n/locales/ja.json
+++ b/web/classic/src/i18n/locales/ja.json
@@ -1180,6 +1180,7 @@
"套餐的基本信息和定价": "プランの基本情報と価格",
"如:大带宽批量分析图片推荐": "例:広帯域での画像一括分析に推奨",
"如:香港线路": "例:香港回線",
+ "开启后,亲和到的渠道被禁用,或不再适用于当前分组/模型时,仍保留这条亲和;关闭时会删除并重新选择渠道。": "有効にすると、アフィニティチャネルが無効化された、または現在のグループ/モデルで利用できなくなった場合でも、そのアフィニティエントリを保持します。無効にすると、エントリを削除して別のチャネルを選択します。",
"如果亲和到的渠道失败,重试到其他渠道成功后,将亲和更新到成功的渠道。": "アフィニティチャネルが失敗した場合、別のチャネルでリトライが成功すると、アフィニティが成功したチャネルに更新されます。",
"如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "New APIなどのリレープロジェクトに接続する場合は、OpenAIタイプを利用してください。設定内容を熟知している場合を除き、このタイプは利用しないでください",
"如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面": "ユーザーリクエストにシステムプロンプトが含まれている場合、この設定内容がユーザーのシステムプロンプトの前に追加されます",
@@ -1555,6 +1556,7 @@
"成功": "成功",
"成功兑换额度:": "引き換え額:",
"成功后切换亲和": "成功時にアフィニティを切り替え",
+ "渠道禁用后保留亲和": "チャネル無効時にアフィニティを保持",
"成功时自动启用通道": "成功時にチャネルを自動的に有効にする",
"我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销": "2要素認証を無効にすると、すべての関連設定とバックアップコードが永久に削除され、この操作は元に戻すことができないことを理解しました",
"我已阅读并同意": "読んで同意します",
diff --git a/web/classic/src/i18n/locales/ru.json b/web/classic/src/i18n/locales/ru.json
index b934dfe1b..94f489a9e 100644
--- a/web/classic/src/i18n/locales/ru.json
+++ b/web/classic/src/i18n/locales/ru.json
@@ -1201,6 +1201,7 @@
"套餐的基本信息和定价": "Основная информация и цена плана",
"如:大带宽批量分析图片推荐": "Например: рекомендуется для пакетного анализа изображений с большой пропускной способностью",
"如:香港线路": "Например: Гонконгская линия",
+ "开启后,亲和到的渠道被禁用,或不再适用于当前分组/模型时,仍保留这条亲和;关闭时会删除并重新选择渠道。": "Если включено, запись аффинити сохраняется, даже когда канал аффинити отключён или больше не подходит для текущей группы/модели. Если выключено, запись будет удалена и выбран другой канал.",
"如果亲和到的渠道失败,重试到其他渠道成功后,将亲和更新到成功的渠道。": "Если канал аффинити не сработал, после успешного повтора на другом канале аффинити будет обновлена на успешный канал.",
"如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "Если вы интегрируетесь с восходящими проектами пересылки, такими как One API или New API, используйте тип OpenAI, не используйте этот тип, если вы не знаете, что делаете.",
"如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面": "Если запрос пользователя содержит системный промпт, используйте эту настройку для добавления перед системным промптом пользователя",
@@ -1602,6 +1603,7 @@
"成功": "Успешно",
"成功兑换额度:": "Успешно обменяно квота: ",
"成功后切换亲和": "Переключить аффинити при успехе",
+ "渠道禁用后保留亲和": "Сохранять аффинити при отключении канала",
"成功时自动启用通道": "Автоматически включать канал при успехе",
"我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销": "Я понимаю, что отключение двухфакторной аутентификации приведет к постоянному удалению всех связанных настроек и резервных кодов, и эта операция не может быть отменена",
"我已阅读并同意": "Я прочитал(а) и согласен(на)",
diff --git a/web/classic/src/i18n/locales/vi.json b/web/classic/src/i18n/locales/vi.json
index 771a25fcf..802f5e291 100644
--- a/web/classic/src/i18n/locales/vi.json
+++ b/web/classic/src/i18n/locales/vi.json
@@ -1181,6 +1181,7 @@
"套餐的基本信息和定价": "Thông tin cơ bản và giá của gói",
"如:大带宽批量分析图片推荐": "ví dụ: Phân tích hàng loạt băng thông lớn đề xuất hình ảnh",
"如:香港线路": "ví dụ: Tuyến Hồng Kông",
+ "开启后,亲和到的渠道被禁用,或不再适用于当前分组/模型时,仍保留这条亲和;关闭时会删除并重新选择渠道。": "Khi bật, giữ mục ưu ái ngay cả khi kênh ưu ái bị tắt hoặc không còn dùng được cho nhóm/mô hình hiện tại. Khi tắt, mục đó sẽ bị xóa và kênh khác sẽ được chọn.",
"如果亲和到的渠道失败,重试到其他渠道成功后,将亲和更新到成功的渠道。": "Nếu kênh ưu ái thất bại, sau khi thử lại thành công trên kênh khác, ưu ái sẽ được cập nhật sang kênh thành công.",
"如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "Nếu bạn đang kết nối với các dự án chuyển tiếp One API hoặc New API thượng nguồn, vui lòng sử dụng loại OpenAI. Đừng sử dụng loại này trừ khi bạn biết mình đang làm gì.",
"如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面": "Nếu yêu cầu của người dùng chứa từ nhắc hệ thống, cài đặt này sẽ được nối vào trước từ nhắc hệ thống của người dùng",
@@ -1556,6 +1557,7 @@
"成功": "Thành công",
"成功兑换额度:": "Số tiền đổi thành công:",
"成功后切换亲和": "Chuyển ưu ái khi thành công",
+ "渠道禁用后保留亲和": "Giữ ưu ái khi kênh bị tắt",
"成功时自动启用通道": "Bật kênh khi thành công",
"我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销": "Tôi đã hiểu rằng việc vô hiệu hóa xác thực hai yếu tố sẽ xóa vĩnh viễn tất cả các cài đặt liên quan và mã dự phòng, thao tác này không thể hoàn tác",
"我已阅读并同意": "Tôi đã đọc và đồng ý với",
diff --git a/web/classic/src/i18n/locales/zh-CN.json b/web/classic/src/i18n/locales/zh-CN.json
index e1141b041..03dac904d 100644
--- a/web/classic/src/i18n/locales/zh-CN.json
+++ b/web/classic/src/i18n/locales/zh-CN.json
@@ -1170,6 +1170,7 @@
"套餐的基本信息和定价": "套餐的基本信息和定价",
"如:大带宽批量分析图片推荐": "如:大带宽批量分析图片推荐",
"如:香港线路": "如:香港线路",
+ "开启后,亲和到的渠道被禁用,或不再适用于当前分组/模型时,仍保留这条亲和;关闭时会删除并重新选择渠道。": "开启后,亲和到的渠道被禁用,或不再适用于当前分组/模型时,仍保留这条亲和;关闭时会删除并重新选择渠道。",
"如果亲和到的渠道失败,重试到其他渠道成功后,将亲和更新到成功的渠道。": "如果亲和到的渠道失败,重试到其他渠道成功后,将亲和更新到成功的渠道。",
"如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。",
"如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面": "如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面",
@@ -1541,6 +1542,7 @@
"成功": "成功",
"成功兑换额度:": "成功兑换额度:",
"成功后切换亲和": "成功后切换亲和",
+ "渠道禁用后保留亲和": "渠道禁用后保留亲和",
"成功时自动启用通道": "成功时自动启用通道",
"我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销": "我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销",
"我已阅读并同意": "我已阅读并同意",
diff --git a/web/classic/src/i18n/locales/zh-TW.json b/web/classic/src/i18n/locales/zh-TW.json
index 3be48fb4d..a94de98e1 100644
--- a/web/classic/src/i18n/locales/zh-TW.json
+++ b/web/classic/src/i18n/locales/zh-TW.json
@@ -1179,6 +1179,7 @@
"套餐的基本信息和定价": "訂閱的基本資訊和定價",
"如:大带宽批量分析图片推荐": "如:大頻寬批量分析圖片推薦",
"如:香港线路": "如:香港線路",
+ "开启后,亲和到的渠道被禁用,或不再适用于当前分组/模型时,仍保留这条亲和;关闭时会删除并重新选择渠道。": "開啟後,親和到的渠道被停用,或不再適用於目前分組/模型時,仍保留這條親和;關閉時會刪除並重新選擇渠道。",
"如果亲和到的渠道失败,重试到其他渠道成功后,将亲和更新到成功的渠道。": "",
"如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "如果你對接的是上游One API或者New API等轉發項目,請使用OpenAI類型,不要使用此類型,除非你知道你在做什麼。",
"如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面": "如果使用者請求中包含系統提示詞,則使用此設定拼接到使用者的系統提示詞前面",
@@ -1551,6 +1552,7 @@
"成功": "成功",
"成功兑换额度:": "成功兌換額度:",
"成功后切换亲和": "",
+ "渠道禁用后保留亲和": "渠道停用後保留親和",
"成功时自动启用通道": "成功時自動啟用通道",
"我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销": "我已瞭解禁用兩步驗證將永久刪除所有相關設定和備用碼,此操作不可撤銷",
"我已阅读并同意": "我已閱讀並同意",
diff --git a/web/classic/src/pages/Setting/Operation/SettingsChannelAffinity.jsx b/web/classic/src/pages/Setting/Operation/SettingsChannelAffinity.jsx
index 66b914432..e3f490e63 100644
--- a/web/classic/src/pages/Setting/Operation/SettingsChannelAffinity.jsx
+++ b/web/classic/src/pages/Setting/Operation/SettingsChannelAffinity.jsx
@@ -62,6 +62,8 @@ import ParamOverrideEditorModal from '../../../components/table/channels/modals/
const KEY_ENABLED = 'channel_affinity_setting.enabled';
const KEY_SWITCH_ON_SUCCESS = 'channel_affinity_setting.switch_on_success';
+const KEY_KEEP_ON_CHANNEL_DISABLED =
+ 'channel_affinity_setting.keep_on_channel_disabled';
const KEY_MAX_ENTRIES = 'channel_affinity_setting.max_entries';
const KEY_DEFAULT_TTL = 'channel_affinity_setting.default_ttl_seconds';
const KEY_RULES = 'channel_affinity_setting.rules';
@@ -241,6 +243,7 @@ export default function SettingsChannelAffinity(props) {
const [inputs, setInputs] = useState({
[KEY_ENABLED]: false,
[KEY_SWITCH_ON_SUCCESS]: true,
+ [KEY_KEEP_ON_CHANNEL_DISABLED]: false,
[KEY_MAX_ENTRIES]: 100000,
[KEY_DEFAULT_TTL]: 3600,
[KEY_RULES]: '[]',
@@ -858,6 +861,7 @@ export default function SettingsChannelAffinity(props) {
![
KEY_ENABLED,
KEY_SWITCH_ON_SUCCESS,
+ KEY_KEEP_ON_CHANNEL_DISABLED,
KEY_MAX_ENTRIES,
KEY_DEFAULT_TTL,
KEY_RULES,
@@ -868,6 +872,8 @@ export default function SettingsChannelAffinity(props) {
currentInputs[key] = toBoolean(props.options[key]);
else if (key === KEY_SWITCH_ON_SUCCESS)
currentInputs[key] = toBoolean(props.options[key]);
+ else if (key === KEY_KEEP_ON_CHANNEL_DISABLED)
+ currentInputs[key] = toBoolean(props.options[key]);
else if (key === KEY_MAX_ENTRIES)
currentInputs[key] = Number(props.options[key] || 0) || 0;
else if (key === KEY_DEFAULT_TTL)
@@ -1003,6 +1009,25 @@ export default function SettingsChannelAffinity(props) {
)}
+
+
+ setInputs({
+ ...inputs,
+ [KEY_KEEP_ON_CHANNEL_DISABLED]: value,
+ })
+ }
+ />
+
+ {t(
+ '开启后,亲和到的渠道被禁用,或不再适用于当前分组/模型时,仍保留这条亲和;关闭时会删除并重新选择渠道。',
+ )}
+
+
diff --git a/web/default/src/features/models/components/drawers/model-mutate-drawer.tsx b/web/default/src/features/models/components/drawers/model-mutate-drawer.tsx
index 5e9cee02b..e603196f7 100644
--- a/web/default/src/features/models/components/drawers/model-mutate-drawer.tsx
+++ b/web/default/src/features/models/components/drawers/model-mutate-drawer.tsx
@@ -196,6 +196,7 @@ export function ModelMutateDrawer({
'grok.violation_deduction_amount': 0,
'channel_affinity_setting.enabled': false,
'channel_affinity_setting.switch_on_success': true,
+ 'channel_affinity_setting.keep_on_channel_disabled': false,
'channel_affinity_setting.max_entries': 100000,
'channel_affinity_setting.default_ttl_seconds': 3600,
'channel_affinity_setting.rules': '[]',
diff --git a/web/default/src/features/system-settings/general/channel-affinity/index.tsx b/web/default/src/features/system-settings/general/channel-affinity/index.tsx
index b24645e17..ba6d02d89 100644
--- a/web/default/src/features/system-settings/general/channel-affinity/index.tsx
+++ b/web/default/src/features/system-settings/general/channel-affinity/index.tsx
@@ -100,6 +100,9 @@ export function ChannelAffinitySection(props: Props) {
const [switchOnSuccess, setSwitchOnSuccess] = useState(
props.defaultValues['channel_affinity_setting.switch_on_success']
)
+ const [keepOnChannelDisabled, setKeepOnChannelDisabled] = useState(
+ props.defaultValues['channel_affinity_setting.keep_on_channel_disabled']
+ )
const [maxEntries, setMaxEntries] = useState(
props.defaultValues['channel_affinity_setting.max_entries']
)
@@ -136,6 +139,9 @@ export function ChannelAffinitySection(props: Props) {
setSwitchOnSuccess(
props.defaultValues['channel_affinity_setting.switch_on_success']
)
+ setKeepOnChannelDisabled(
+ props.defaultValues['channel_affinity_setting.keep_on_channel_disabled']
+ )
setMaxEntries(props.defaultValues['channel_affinity_setting.max_entries'])
setDefaultTtl(
props.defaultValues['channel_affinity_setting.default_ttl_seconds']
@@ -231,6 +237,14 @@ export function ChannelAffinitySection(props: Props) {
key: 'channel_affinity_setting.switch_on_success',
value: String(switchOnSuccess),
})
+ if (
+ keepOnChannelDisabled !==
+ props.defaultValues['channel_affinity_setting.keep_on_channel_disabled']
+ )
+ updates.push({
+ key: 'channel_affinity_setting.keep_on_channel_disabled',
+ value: String(keepOnChannelDisabled),
+ })
if (
maxEntries !==
props.defaultValues['channel_affinity_setting.max_entries']
@@ -397,6 +411,14 @@ export function ChannelAffinitySection(props: Props) {
'If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.'
)}
/>
+
diff --git a/web/default/src/features/system-settings/general/channel-affinity/types.ts b/web/default/src/features/system-settings/general/channel-affinity/types.ts
index de38b74ef..23623c8c1 100644
--- a/web/default/src/features/system-settings/general/channel-affinity/types.ts
+++ b/web/default/src/features/system-settings/general/channel-affinity/types.ts
@@ -50,6 +50,7 @@ export interface CacheStats {
export interface ChannelAffinitySettings {
'channel_affinity_setting.enabled': boolean
'channel_affinity_setting.switch_on_success': boolean
+ 'channel_affinity_setting.keep_on_channel_disabled': boolean
'channel_affinity_setting.max_entries': number
'channel_affinity_setting.default_ttl_seconds': number
'channel_affinity_setting.rules': string
diff --git a/web/default/src/features/system-settings/models/index.tsx b/web/default/src/features/system-settings/models/index.tsx
index 050235ac7..abec0bf99 100644
--- a/web/default/src/features/system-settings/models/index.tsx
+++ b/web/default/src/features/system-settings/models/index.tsx
@@ -64,6 +64,7 @@ const defaultModelSettings: ModelSettings = {
'group_ratio_setting.group_special_usable_group': '{}',
'channel_affinity_setting.enabled': false,
'channel_affinity_setting.switch_on_success': true,
+ 'channel_affinity_setting.keep_on_channel_disabled': false,
'channel_affinity_setting.max_entries': 100000,
'channel_affinity_setting.default_ttl_seconds': 3600,
'channel_affinity_setting.rules': '[]',
diff --git a/web/default/src/features/system-settings/models/section-registry.tsx b/web/default/src/features/system-settings/models/section-registry.tsx
index 2fe3d8e85..dd3ae7d5d 100644
--- a/web/default/src/features/system-settings/models/section-registry.tsx
+++ b/web/default/src/features/system-settings/models/section-registry.tsx
@@ -130,6 +130,8 @@ const MODELS_SECTIONS = [
settings['channel_affinity_setting.enabled'],
'channel_affinity_setting.switch_on_success':
settings['channel_affinity_setting.switch_on_success'],
+ 'channel_affinity_setting.keep_on_channel_disabled':
+ settings['channel_affinity_setting.keep_on_channel_disabled'],
'channel_affinity_setting.max_entries':
settings['channel_affinity_setting.max_entries'],
'channel_affinity_setting.default_ttl_seconds':
diff --git a/web/default/src/features/system-settings/types.ts b/web/default/src/features/system-settings/types.ts
index 212226807..962bc9cce 100644
--- a/web/default/src/features/system-settings/types.ts
+++ b/web/default/src/features/system-settings/types.ts
@@ -176,6 +176,7 @@ export type ModelSettings = {
'group_ratio_setting.group_special_usable_group': string
'channel_affinity_setting.enabled': boolean
'channel_affinity_setting.switch_on_success': boolean
+ 'channel_affinity_setting.keep_on_channel_disabled': boolean
'channel_affinity_setting.max_entries': number
'channel_affinity_setting.default_ttl_seconds': number
'channel_affinity_setting.rules': string
diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json
index 7abaed071..e143d184d 100644
--- a/web/default/src/i18n/locales/en.json
+++ b/web/default/src/i18n/locales/en.json
@@ -644,6 +644,7 @@
"Channel Affinity": "Channel Affinity",
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.",
"Channel Affinity: Upstream Cache Hit": "Channel Affinity: Upstream Cache Hit",
+ "Keep affinity when channel is disabled": "Keep affinity when channel is disabled",
"Channel copied successfully": "Channel copied successfully",
"Channel created successfully": "Channel created successfully",
"Channel deleted successfully": "Channel deleted successfully",
@@ -1994,6 +1995,7 @@
"If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing",
"If default auto group is enabled, newly created tokens start with auto instead of an empty group.": "If default auto group is enabled, newly created tokens start with auto instead of an empty group.",
"If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.",
+ "When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.",
"If this keeps happening, please report it on GitHub Issues.": "If this keeps happening, please report it on GitHub Issues.",
"If you provide generative AI services to the public in mainland China, you will fulfill legal obligations including filing, security assessment, content safety, complaint handling, generated content labeling, log retention, and personal information protection.": "If you provide generative AI services to the public in mainland China, you will fulfill legal obligations including filing, security assessment, content safety, complaint handling, generated content labeling, log retention, and personal information protection.",
"Ignored upstream models": "Ignored upstream models",
diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json
index 27e3ea499..cfeb3da2a 100644
--- a/web/default/src/i18n/locales/fr.json
+++ b/web/default/src/i18n/locales/fr.json
@@ -644,6 +644,7 @@
"Channel Affinity": "Affinité de canal",
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "L'affinité de canal réutilise le dernier canal ayant réussi, en se basant sur les clés extraites du contexte de la requête ou du corps JSON.",
"Channel Affinity: Upstream Cache Hit": "Affinité de canal : hit de cache en amont",
+ "Keep affinity when channel is disabled": "Conserver l'affinité lorsque le canal est désactivé",
"Channel copied successfully": "Canal copié avec succès",
"Channel created successfully": "Canal créé avec succès",
"Channel deleted successfully": "Canal supprimé avec succès",
@@ -1994,6 +1995,7 @@
"If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "Si vous vous connectez à des projets de relais One API ou New API en amont, utilisez le type OpenAI à la place sauf si vous savez ce que vous faites",
"If default auto group is enabled, newly created tokens start with auto instead of an empty group.": "Si le groupe auto par défaut est activé, les nouveaux jetons commencent avec auto au lieu d’un groupe vide.",
"If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "Si le canal affinitaire échoue et qu'une nouvelle tentative réussit sur un autre canal, mettre à jour l'affinité vers le canal ayant réussi.",
+ "When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "Lorsque cette option est activée, conserver l'entrée d'affinité même si le canal affinitaire est désactivé ou n'est plus utilisable pour le groupe/modèle actuel. Laissez-la désactivée pour supprimer l'entrée et sélectionner un autre canal.",
"If this keeps happening, please report it on GitHub Issues.": "Si cela continue, veuillez le signaler sur GitHub Issues.",
"If you provide generative AI services to the public in mainland China, you will fulfill legal obligations including filing, security assessment, content safety, complaint handling, generated content labeling, log retention, and personal information protection.": "Si vous fournissez des services d’IA générative au public en Chine continentale, vous remplirez les obligations légales applicables, notamment le dépôt, l’évaluation de sécurité, la sécurité du contenu, le traitement des plaintes, l’étiquetage du contenu généré, la conservation des journaux et la protection des informations personnelles.",
"Ignored upstream models": "Modèles amont ignorés",
diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json
index 26ed41748..06f3a14b1 100644
--- a/web/default/src/i18n/locales/ja.json
+++ b/web/default/src/i18n/locales/ja.json
@@ -644,6 +644,7 @@
"Channel Affinity": "チャネルアフィニティ",
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "チャネルアフィニティは、リクエストコンテキストまたは JSON Body から抽出したキーに基づいて、前回成功したチャネルを優先的に再利用します。",
"Channel Affinity: Upstream Cache Hit": "チャネルアフィニティ:上流キャッシュヒット",
+ "Keep affinity when channel is disabled": "チャネル無効時にアフィニティを保持",
"Channel copied successfully": "チャンネルが正常にコピーされました",
"Channel created successfully": "チャンネルが正常に作成されました",
"Channel deleted successfully": "チャンネルが正常に削除されました",
@@ -1994,6 +1995,7 @@
"If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "上流の One API または New API リレープロジェクトに接続する場合、知っている場合を除き OpenAI タイプを使用してください",
"If default auto group is enabled, newly created tokens start with auto instead of an empty group.": "デフォルト auto グループを有効にすると、新規トークンは空グループではなく auto で開始します。",
"If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "アフィニティチャネルが失敗し、別のチャネルでリトライが成功した場合、アフィニティを成功したチャネルに更新します。",
+ "When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "有効にすると、アフィニティチャネルが無効化された、または現在のグループ/モデルで利用できなくなった場合でも、そのアフィニティエントリを保持します。無効のままにすると、エントリを削除して別のチャネルを選択します。",
"If this keeps happening, please report it on GitHub Issues.": "この問題が続く場合は、GitHub Issues で報告してください。",
"If you provide generative AI services to the public in mainland China, you will fulfill legal obligations including filing, security assessment, content safety, complaint handling, generated content labeling, log retention, and personal information protection.": "中国本土で一般向けに生成 AI サービスを提供する場合、届出、セキュリティ評価、コンテンツ安全、苦情対応、生成コンテンツのラベル表示、ログ保存、個人情報保護などの法的義務を履行します。",
"Ignored upstream models": "無視する上流モデル",
diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json
index 50382fd0f..90665a853 100644
--- a/web/default/src/i18n/locales/ru.json
+++ b/web/default/src/i18n/locales/ru.json
@@ -644,6 +644,7 @@
"Channel Affinity": "Привязка к каналу",
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Привязка к каналу повторно использует последний успешный канал на основе ключей, извлечённых из контекста запроса или тела JSON.",
"Channel Affinity: Upstream Cache Hit": "Привязка к каналу: попадание в кэш upstream",
+ "Keep affinity when channel is disabled": "Сохранять привязку при отключении канала",
"Channel copied successfully": "Канал успешно скопирован",
"Channel created successfully": "Канал успешно создан",
"Channel deleted successfully": "Канал успешно удалён",
@@ -1994,6 +1995,7 @@
"If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "При подключении к upstream One API или проектам-ретрансляторам New API используйте тип OpenAI, если только вы точно знаете, что делаете",
"If default auto group is enabled, newly created tokens start with auto instead of an empty group.": "Если группа auto включена по умолчанию, новые токены создаются с auto вместо пустой группы.",
"If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "Если привязанный канал не работает и повторная попытка удалась через другой канал, привязка обновляется на успешный канал.",
+ "When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "Если включено, запись привязки сохраняется, даже когда привязанный канал отключён или больше не подходит для текущей группы/модели. Оставьте выключенным, чтобы удалять запись и выбирать другой канал.",
"If this keeps happening, please report it on GitHub Issues.": "Если проблема повторяется, сообщите о ней в GitHub Issues.",
"If you provide generative AI services to the public in mainland China, you will fulfill legal obligations including filing, security assessment, content safety, complaint handling, generated content labeling, log retention, and personal information protection.": "Если вы предоставляете услуги генеративного ИИ населению материкового Китая, вы будете выполнять юридические обязанности, включая регистрацию, оценку безопасности, безопасность контента, обработку жалоб, маркировку сгенерированного контента, хранение журналов и защиту персональных данных.",
"Ignored upstream models": "Игнорируемые upstream-модели",
diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json
index a28b90fee..db40cb5c2 100644
--- a/web/default/src/i18n/locales/vi.json
+++ b/web/default/src/i18n/locales/vi.json
@@ -644,6 +644,7 @@
"Channel Affinity": "Ưu tiên kênh",
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Ưu tiên kênh sẽ sử dụng lại kênh thành công gần nhất dựa trên các khóa được trích xuất từ ngữ cảnh yêu cầu hoặc JSON body.",
"Channel Affinity: Upstream Cache Hit": "Ưu tiên kênh: Cache hit từ upstream",
+ "Keep affinity when channel is disabled": "Giữ ưu tiên khi kênh bị tắt",
"Channel copied successfully": "Sao chép kênh thành công",
"Channel created successfully": "Tạo kênh thành công",
"Channel deleted successfully": "Xóa kênh thành công",
@@ -1994,6 +1995,7 @@
"If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "Nếu kết nối với dự án relay One API hoặc New API upstream, hãy sử dụng loại OpenAI thay thế trừ khi bạn biết mình đang làm gì",
"If default auto group is enabled, newly created tokens start with auto instead of an empty group.": "Nếu bật nhóm auto mặc định, token mới sẽ bắt đầu với auto thay vì nhóm trống.",
"If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "Nếu kênh ưu tiên thất bại và thử lại thành công trên kênh khác, cập nhật ưu tiên sang kênh thành công.",
+ "When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "Khi bật, giữ mục ưu tiên ngay cả khi kênh ưu tiên bị tắt hoặc không còn dùng được cho nhóm/mô hình hiện tại. Để tắt để xóa mục đó và chọn kênh khác.",
"If this keeps happening, please report it on GitHub Issues.": "Nếu sự cố tiếp tục xảy ra, vui lòng báo cáo trên GitHub Issues.",
"If you provide generative AI services to the public in mainland China, you will fulfill legal obligations including filing, security assessment, content safety, complaint handling, generated content labeling, log retention, and personal information protection.": "Nếu bạn cung cấp dịch vụ AI tạo sinh cho công chúng tại Trung Quốc đại lục, bạn sẽ thực hiện các nghĩa vụ pháp lý bao gồm đăng ký, đánh giá an toàn, an toàn nội dung, xử lý khiếu nại, gắn nhãn nội dung được tạo, lưu giữ nhật ký và bảo vệ thông tin cá nhân.",
"Ignored upstream models": "Mô hình upstream bị bỏ qua",
diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json
index 1ce821f56..772b36c58 100644
--- a/web/default/src/i18n/locales/zh.json
+++ b/web/default/src/i18n/locales/zh.json
@@ -644,6 +644,7 @@
"Channel Affinity": "渠道亲和性",
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "渠道亲和性会基于从请求上下文或 JSON Body 提取的 Key,优先复用上一次成功的渠道。",
"Channel Affinity: Upstream Cache Hit": "渠道亲和性:上游缓存命中",
+ "Keep affinity when channel is disabled": "渠道禁用后保留亲和",
"Channel copied successfully": "渠道复制成功",
"Channel created successfully": "渠道创建成功",
"Channel deleted successfully": "渠道删除成功",
@@ -1994,6 +1995,7 @@
"If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "如果连接上游 One API 或 New API 中继项目,除非您知道自己在做什么,否则请使用 OpenAI 类型",
"If default auto group is enabled, newly created tokens start with auto instead of an empty group.": "如果启用默认 auto 分组,新建令牌会默认使用 auto,而不是空分组。",
"If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "如果亲和到的渠道失败,重试到其他渠道成功后,将亲和更新到成功的渠道。",
+ "When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "开启后,亲和到的渠道被禁用,或不再适用于当前分组/模型时,仍保留这条亲和;关闭时会删除并重新选择渠道。",
"If this keeps happening, please report it on GitHub Issues.": "如果问题持续出现,请到 GitHub Issues 反馈。",
"If you provide generative AI services to the public in mainland China, you will fulfill legal obligations including filing, security assessment, content safety, complaint handling, generated content labeling, log retention, and personal information protection.": "如果你在中国大陆向公众提供生成式人工智能服务,你将履行备案、安全评估、内容安全、投诉处理、生成内容标识、日志留存和个人信息保护等法律义务。",
"Ignored upstream models": "已忽略上游模型",