From 50eddc01f99bafbaf2efbcc48f4b93f3d49cbc81 Mon Sep 17 00:00:00 2001 From: SpringNyan Date: Wed, 22 Apr 2026 16:50:21 +0000 Subject: [PATCH 1/2] feat: support channel alias lookup via remark field Allow channels to define an alias in their remark using [alias=name] format. The distributor first tries alias lookup, then falls back to numeric ID for token-specific channel routing. --- middleware/distributor.go | 19 +++++++++++-------- model/channel.go | 14 ++++++++++++++ model/channel_cache.go | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/middleware/distributor.go b/middleware/distributor.go index d626941456c..1e05ae5f98c 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -37,15 +37,18 @@ func Distribute() func(c *gin.Context) { return } if ok { - id, err := strconv.Atoi(channelId.(string)) + channel, err = model.CacheGetChannelByAlias(channelId.(string)) if err != nil { - abortWithOpenAiMessage(c, http.StatusBadRequest, i18n.T(c, i18n.MsgDistributorInvalidChannelId)) - return - } - channel, err = model.GetChannelById(id, true) - if err != nil { - abortWithOpenAiMessage(c, http.StatusBadRequest, i18n.T(c, i18n.MsgDistributorInvalidChannelId)) - return + id, err := strconv.Atoi(channelId.(string)) + if err != nil { + abortWithOpenAiMessage(c, http.StatusBadRequest, i18n.T(c, i18n.MsgDistributorInvalidChannelId)) + return + } + channel, err = model.GetChannelById(id, true) + if err != nil { + abortWithOpenAiMessage(c, http.StatusBadRequest, i18n.T(c, i18n.MsgDistributorInvalidChannelId)) + return + } } if channel.Status != common.ChannelStatusEnabled { abortWithOpenAiMessage(c, http.StatusForbidden, i18n.T(c, i18n.MsgDistributorChannelDisabled)) diff --git a/model/channel.go b/model/channel.go index f256b54ce35..8a4ead59134 100644 --- a/model/channel.go +++ b/model/channel.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "math/rand" + "regexp" "strings" "sync" @@ -242,6 +243,19 @@ func (channel *Channel) SetTag(tag string) { channel.Tag = &tag } +var aliasRegexp = regexp.MustCompile(`(?m)^\[alias=([a-zA-Z0-9_]+)\]$`) + +func (channel *Channel) GetAlias() string { + if channel.Remark == nil || *channel.Remark == "" { + return "" + } + matches := aliasRegexp.FindStringSubmatch(*channel.Remark) + if len(matches) < 2 { + return "" + } + return matches[1] +} + func (channel *Channel) GetAutoBan() bool { if channel.AutoBan == nil { return false diff --git a/model/channel_cache.go b/model/channel_cache.go index c9c50357603..ffb21781ac9 100644 --- a/model/channel_cache.go +++ b/model/channel_cache.go @@ -16,6 +16,7 @@ import ( var group2model2channels map[string]map[string][]int // enabled channel var channelsIDM map[int]*Channel // all channels include disabled +var channelsAliasM map[string]int // alias -> channel ID var channelSyncLock sync.RWMutex func InitChannelCache() { @@ -28,6 +29,15 @@ func InitChannelCache() { for _, channel := range channels { newChannelId2channel[channel.Id] = channel } + newChannelsAliasM := make(map[string]int) + for _, channel := range channels { + if alias := channel.GetAlias(); alias != "" { + if _, exists := newChannelsAliasM[alias]; exists { + common.SysLog(fmt.Sprintf("duplicate channel alias '%s', channel %d will overwrite", alias, channel.Id)) + } + newChannelsAliasM[alias] = channel.Id + } + } var abilities []*Ability DB.Find(&abilities) groups := make(map[string]bool) @@ -81,6 +91,7 @@ func InitChannelCache() { } } channelsIDM = newChannelId2channel + channelsAliasM = newChannelsAliasM channelSyncLock.Unlock() common.SysLog("channels synced from database") } @@ -260,6 +271,32 @@ func CacheUpdateChannel(channel *Channel) { println("CacheUpdateChannel:", channel.Id, channel.Name, channel.Status, channel.ChannelInfo.MultiKeyPollingIndex) println("before:", channelsIDM[channel.Id].ChannelInfo.MultiKeyPollingIndex) + // Remove old alias if remark changed + if old, ok := channelsIDM[channel.Id]; ok { + if oldAlias := old.GetAlias(); oldAlias != "" { + delete(channelsAliasM, oldAlias) + } + } + if newAlias := channel.GetAlias(); newAlias != "" { + channelsAliasM[newAlias] = channel.Id + } channelsIDM[channel.Id] = channel println("after :", channelsIDM[channel.Id].ChannelInfo.MultiKeyPollingIndex) } + +func CacheGetChannelByAlias(alias string) (*Channel, error) { + if !common.MemoryCacheEnabled { + return nil, fmt.Errorf("channel alias requires memory cache to be enabled") + } + channelSyncLock.RLock() + defer channelSyncLock.RUnlock() + id, ok := channelsAliasM[alias] + if !ok { + return nil, fmt.Errorf("no channel found with alias '%s'", alias) + } + c, ok := channelsIDM[id] + if !ok { + return nil, fmt.Errorf("channel #%d referenced by alias '%s' not found", id, alias) + } + return c, nil +} From 01c58a678e91daf1b0d30e6a3c4416b2b09c7310 Mon Sep 17 00:00:00 2001 From: SpringNyan Date: Thu, 23 Apr 2026 14:59:07 +0000 Subject: [PATCH 2/2] feat: support stripping v1 version prefix from upstream request URL via remark --- constant/context_key.go | 1 + middleware/distributor.go | 2 ++ model/channel.go | 9 +++++++++ relay/channel/openai/adaptor.go | 3 +++ relay/common/relay_info.go | 11 +++++++++++ 5 files changed, 26 insertions(+) diff --git a/constant/context_key.go b/constant/context_key.go index c28ad202514..e861d846d34 100644 --- a/constant/context_key.go +++ b/constant/context_key.go @@ -37,6 +37,7 @@ const ( ContextKeyChannelIsMultiKey ContextKey = "channel_is_multi_key" ContextKeyChannelMultiKeyIndex ContextKey = "channel_multi_key_index" ContextKeyChannelKey ContextKey = "channel_key" + ContextKeyChannelStripV1Version ContextKey = "channel_strip_v1_version" ContextKeyAutoGroup ContextKey = "auto_group" ContextKeyAutoGroupIndex ContextKey = "auto_group_index" diff --git a/middleware/distributor.go b/middleware/distributor.go index 1e05ae5f98c..7bd93ccbb60 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -387,6 +387,8 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode common.SetContextKey(c, constant.ContextKeySystemPromptOverride, false) + common.SetContextKey(c, constant.ContextKeyChannelStripV1Version, channel.GetStripV1Version()) + // TODO: api_version统一 switch channel.Type { case constant.ChannelTypeAzure: diff --git a/model/channel.go b/model/channel.go index 8a4ead59134..3577e2fe19b 100644 --- a/model/channel.go +++ b/model/channel.go @@ -256,6 +256,15 @@ func (channel *Channel) GetAlias() string { return matches[1] } +var stripV1VersionRegexp = regexp.MustCompile(`(?m)^\[strip_v1_version=true\]$`) + +func (channel *Channel) GetStripV1Version() bool { + if channel.Remark == nil || *channel.Remark == "" { + return false + } + return stripV1VersionRegexp.MatchString(*channel.Remark) +} + func (channel *Channel) GetAutoBan() bool { if channel.AutoBan == nil { return false diff --git a/relay/channel/openai/adaptor.go b/relay/channel/openai/adaptor.go index 56a58f28652..1d377ba8385 100644 --- a/relay/channel/openai/adaptor.go +++ b/relay/channel/openai/adaptor.go @@ -180,6 +180,9 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { if (info.RelayFormat == types.RelayFormatClaude || info.RelayFormat == types.RelayFormatGemini) && info.RelayMode != relayconstant.RelayModeResponses && info.RelayMode != relayconstant.RelayModeResponsesCompact { + if info.StripV1Version { + return fmt.Sprintf("%s/chat/completions", info.ChannelBaseUrl), nil + } return fmt.Sprintf("%s/v1/chat/completions", info.ChannelBaseUrl), nil } return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, info.RequestURLPath, info.ChannelType), nil diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index 2e157fc8cd9..dad551d9d94 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "regexp" "strconv" "strings" "time" @@ -19,6 +20,8 @@ import ( "github.com/gorilla/websocket" ) +var versionPrefixRegexp = regexp.MustCompile(`^/(v1|v1beta)/`) + type ThinkingContentInfo struct { IsFirstThinkingContent bool SendLastThinkingContent bool @@ -76,6 +79,7 @@ type ChannelMeta struct { UpstreamModelName string IsModelMapped bool SupportStreamOptions bool // 是否支持流式选项 + StripV1Version bool } type TokenCountMeta struct { @@ -195,6 +199,7 @@ func (info *RelayInfo) InitChannelMeta(c *gin.Context) { UpstreamModelName: common.GetContextKeyString(c, constant.ContextKeyOriginalModel), IsModelMapped: false, SupportStreamOptions: false, + StripV1Version: common.GetContextKeyBool(c, constant.ContextKeyChannelStripV1Version), } if channelType == constant.ChannelTypeAzure { @@ -220,6 +225,12 @@ func (info *RelayInfo) InitChannelMeta(c *gin.Context) { info.ChannelMeta = channelMeta + if channelMeta.StripV1Version { + if versionPrefixRegexp.MatchString(info.RequestURLPath) { + info.RequestURLPath = "/" + versionPrefixRegexp.ReplaceAllString(info.RequestURLPath, "") + } + } + // reset some fields based on channel meta // 重置某些字段,例如模型名称等 if info.Request != nil {