From bb256b89cd754005d5c433b97be90375c049eaa1 Mon Sep 17 00:00:00 2001 From: "sean.opencode" Date: Sat, 6 Jun 2026 14:54:12 +0000 Subject: [PATCH] feat: support v2/v3 protocol version prefix in upstream channel baseURL - Add BaseUrlHasVersionPrefix/StripVersionPrefix helpers in relay/common - Auto-strip /v1 from requestURL when baseURL already contains version prefix - Normalize version prefix in path-to-relay-mode matching - Update channel billing, models fetch, upstream sync, video proxy to use GetFullRequestURL - Update deepseek, moonshot, openai, perplexity, siliconflow adaptors --- controller/channel-billing.go | 5 +-- controller/channel.go | 3 +- controller/channel_upstream_update.go | 9 ++--- controller/video_proxy.go | 3 +- relay/channel/deepseek/adaptor.go | 2 +- relay/channel/moonshot/adaptor.go | 10 +++--- relay/channel/openai/adaptor.go | 5 ++- relay/channel/perplexity/adaptor.go | 2 +- relay/channel/siliconflow/adaptor.go | 2 +- relay/common/relay_utils.go | 49 ++++++++++++++++++++++++--- relay/constant/relay_mode.go | 20 +++++++++++ 11 files changed, 88 insertions(+), 22 deletions(-) diff --git a/controller/channel-billing.go b/controller/channel-billing.go index 751ee3600ac9..c9aa53397761 100644 --- a/controller/channel-billing.go +++ b/controller/channel-billing.go @@ -13,6 +13,7 @@ import ( "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/service" + relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/setting/operation_setting" "github.com/QuantumNous/new-api/types" @@ -389,7 +390,7 @@ func updateChannelBalance(channel *model.Channel) (float64, error) { default: return 0, errors.New("尚未实现") } - url := fmt.Sprintf("%s/v1/dashboard/billing/subscription", baseURL) + url := relaycommon.GetFullRequestURL(baseURL, "/v1/dashboard/billing/subscription", channel.Type) body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) if err != nil { @@ -406,7 +407,7 @@ func updateChannelBalance(channel *model.Channel) (float64, error) { if !subscription.HasPaymentMethod { startDate = now.AddDate(0, 0, -100).Format("2006-01-02") } - url = fmt.Sprintf("%s/v1/dashboard/billing/usage?start_date=%s&end_date=%s", baseURL, startDate, endDate) + url = relaycommon.GetFullRequestURL(baseURL, fmt.Sprintf("/v1/dashboard/billing/usage?start_date=%s&end_date=%s", startDate, endDate), channel.Type) body, err = GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) if err != nil { return 0, err diff --git a/controller/channel.go b/controller/channel.go index c59e492a5a02..11d2e9260222 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -14,6 +14,7 @@ import ( "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/model" relaychannel "github.com/QuantumNous/new-api/relay/channel" + relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/relay/channel/gemini" "github.com/QuantumNous/new-api/relay/channel/ollama" "github.com/QuantumNous/new-api/service" @@ -1055,7 +1056,7 @@ func FetchModels(c *gin.Context) { } client := &http.Client{} - url := fmt.Sprintf("%s/v1/models", baseURL) + url := relaycommon.GetFullRequestURL(baseURL, "/v1/models", req.Type) request, err := http.NewRequest("GET", url, nil) if err != nil { diff --git a/controller/channel_upstream_update.go b/controller/channel_upstream_update.go index 77a1e3c817a8..f475079c76ec 100644 --- a/controller/channel_upstream_update.go +++ b/controller/channel_upstream_update.go @@ -16,6 +16,7 @@ import ( "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/relay/channel/gemini" "github.com/QuantumNous/new-api/relay/channel/ollama" + relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/service" "github.com/gin-gonic/gin" @@ -301,18 +302,18 @@ func fetchChannelUpstreamModelIDs(channel *model.Channel) ([]string, error) { } case constant.ChannelTypeVolcEngine: if plan, ok := constant.ChannelSpecialBases[baseURL]; ok && plan.OpenAIBaseURL != "" { - url = fmt.Sprintf("%s/v1/models", plan.OpenAIBaseURL) + url = relaycommon.GetFullRequestURL(plan.OpenAIBaseURL, "/v1/models", channel.Type) } else { - url = fmt.Sprintf("%s/v1/models", baseURL) + url = relaycommon.GetFullRequestURL(baseURL, "/v1/models", channel.Type) } case constant.ChannelTypeMoonshot: if plan, ok := constant.ChannelSpecialBases[baseURL]; ok && plan.OpenAIBaseURL != "" { url = fmt.Sprintf("%s/models", plan.OpenAIBaseURL) } else { - url = fmt.Sprintf("%s/v1/models", baseURL) + url = relaycommon.GetFullRequestURL(baseURL, "/v1/models", channel.Type) } default: - url = fmt.Sprintf("%s/v1/models", baseURL) + url = relaycommon.GetFullRequestURL(baseURL, "/v1/models", channel.Type) } key, _, apiErr := channel.GetNextEnabledKey() diff --git a/controller/video_proxy.go b/controller/video_proxy.go index 520d313a312d..8072d0fdf760 100644 --- a/controller/video_proxy.go +++ b/controller/video_proxy.go @@ -15,6 +15,7 @@ import ( "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/service" + relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/QuantumNous/new-api/setting/system_setting" "github.com/gin-gonic/gin" @@ -107,7 +108,7 @@ func VideoProxy(c *gin.Context) { return } case constant.ChannelTypeOpenAI, constant.ChannelTypeSora: - videoURL = fmt.Sprintf("%s/v1/videos/%s/content", baseURL, task.GetUpstreamTaskID()) + videoURL = relaycommon.GetFullRequestURL(baseURL, fmt.Sprintf("/v1/videos/%s/content", task.GetUpstreamTaskID()), channel.Type) req.Header.Set("Authorization", "Bearer "+channel.Key) default: // Video URL is stored in PrivateData.ResultURL (fallback to FailReason for old data) diff --git a/relay/channel/deepseek/adaptor.go b/relay/channel/deepseek/adaptor.go index 60eaf22be568..07f81464333e 100644 --- a/relay/channel/deepseek/adaptor.go +++ b/relay/channel/deepseek/adaptor.go @@ -69,7 +69,7 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { case constant.RelayModeCompletions: return fmt.Sprintf("%s/completions", fimBaseUrl), nil default: - return fmt.Sprintf("%s/v1/chat/completions", info.ChannelBaseUrl), nil + return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, "/v1/chat/completions", info.ChannelType), nil } } } diff --git a/relay/channel/moonshot/adaptor.go b/relay/channel/moonshot/adaptor.go index c2f6ee4a4b2d..85a421f2e7a4 100644 --- a/relay/channel/moonshot/adaptor.go +++ b/relay/channel/moonshot/adaptor.go @@ -60,15 +60,15 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { return fmt.Sprintf("%s/anthropic/v1/messages", info.ChannelBaseUrl), nil default: if info.RelayMode == constant.RelayModeRerank { - return fmt.Sprintf("%s/v1/rerank", info.ChannelBaseUrl), nil + return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, "/v1/rerank", info.ChannelType), nil } else if info.RelayMode == constant.RelayModeEmbeddings { - return fmt.Sprintf("%s/v1/embeddings", info.ChannelBaseUrl), nil + return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, "/v1/embeddings", info.ChannelType), nil } else if info.RelayMode == constant.RelayModeChatCompletions { - return fmt.Sprintf("%s/v1/chat/completions", info.ChannelBaseUrl), nil + return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, "/v1/chat/completions", info.ChannelType), nil } else if info.RelayMode == constant.RelayModeCompletions { - return fmt.Sprintf("%s/v1/completions", info.ChannelBaseUrl), nil + return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, "/v1/completions", info.ChannelType), nil } - return fmt.Sprintf("%s/v1/chat/completions", info.ChannelBaseUrl), nil + return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, "/v1/chat/completions", info.ChannelType), nil } } diff --git a/relay/channel/openai/adaptor.go b/relay/channel/openai/adaptor.go index 26fac8e791c8..b1fae0a63100 100644 --- a/relay/channel/openai/adaptor.go +++ b/relay/channel/openai/adaptor.go @@ -115,7 +115,7 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { // https://learn.microsoft.com/en-us/azure/cognitive-services/openai/chatgpt-quickstart?pivots=rest-api&tabs=command-line#rest-api requestURL := strings.Split(info.RequestURLPath, "?")[0] requestURL = fmt.Sprintf("%s?api-version=%s", requestURL, apiVersion) - task := strings.TrimPrefix(requestURL, "/v1/") + task := relaycommon.StripVersionPrefix(requestURL) if info.RelayFormat == types.RelayFormatClaude { task = strings.TrimPrefix(task, "messages") @@ -166,6 +166,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 relaycommon.BaseUrlHasVersionPrefix(info.ChannelBaseUrl) { + 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/channel/perplexity/adaptor.go b/relay/channel/perplexity/adaptor.go index 6b0369094503..de8019c727ee 100644 --- a/relay/channel/perplexity/adaptor.go +++ b/relay/channel/perplexity/adaptor.go @@ -45,7 +45,7 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) { func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { if info.RelayMode == relayconstant.RelayModeResponses { - return fmt.Sprintf("%s/v1/responses", info.ChannelBaseUrl), nil + return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, "/v1/responses", info.ChannelType), nil } return fmt.Sprintf("%s/chat/completions", info.ChannelBaseUrl), nil } diff --git a/relay/channel/siliconflow/adaptor.go b/relay/channel/siliconflow/adaptor.go index 3e9bee55adf6..59b5e1d030ce 100644 --- a/relay/channel/siliconflow/adaptor.go +++ b/relay/channel/siliconflow/adaptor.go @@ -67,7 +67,7 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) { func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { if info.RelayMode == constant.RelayModeRerank { - return fmt.Sprintf("%s/v1/rerank", info.ChannelBaseUrl), nil + return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, "/v1/rerank", info.ChannelType), nil } return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, info.RequestURLPath, info.ChannelType), nil } diff --git a/relay/common/relay_utils.go b/relay/common/relay_utils.go index 18df77a645d6..0e9acd4889fc 100644 --- a/relay/common/relay_utils.go +++ b/relay/common/relay_utils.go @@ -3,6 +3,7 @@ package common import ( "fmt" "net/http" + "net/url" "strconv" "strings" @@ -22,18 +23,56 @@ type HasImage interface { HasImage() bool } -func GetFullRequestURL(baseURL string, requestURL string, channelType int) string { - fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL) +func BaseUrlHasVersionPrefix(baseURL string) bool { + u, err := url.Parse(baseURL) + if err != nil { + return false + } + path := strings.Trim(u.Path, "/") + parts := strings.Split(path, "/") + for _, part := range parts { + if strings.HasPrefix(part, "v") && len(part) > 1 { + if part[1] >= '0' && part[1] <= '9' { + return true + } + } + } + return false +} +func StripVersionPrefix(path string) string { + if len(path) < 3 || path[0] != '/' { + return path + } + secondSlash := strings.Index(path[1:], "/") + if secondSlash == -1 { + return path + } + secondSlash++ + prefix := path[:secondSlash] + if strings.HasPrefix(prefix, "/v") && len(prefix) > 2 { + verPart := prefix[2:] + if len(verPart) > 0 && verPart[0] >= '0' && verPart[0] <= '9' { + return path[secondSlash+1:] + } + } + return path +} + +func GetFullRequestURL(baseURL string, requestURL string, channelType int) string { if strings.HasPrefix(baseURL, "https://gateway.ai.cloudflare.com") { switch channelType { case constant.ChannelTypeOpenAI: - fullRequestURL = fmt.Sprintf("%s%s", baseURL, strings.TrimPrefix(requestURL, "/v1")) + return fmt.Sprintf("%s%s", baseURL, strings.TrimPrefix(requestURL, "/v1")) case constant.ChannelTypeAzure: - fullRequestURL = fmt.Sprintf("%s%s", baseURL, strings.TrimPrefix(requestURL, "/openai/deployments")) + return fmt.Sprintf("%s%s", baseURL, strings.TrimPrefix(requestURL, "/openai/deployments")) } } - return fullRequestURL + + if BaseUrlHasVersionPrefix(baseURL) && strings.HasPrefix(requestURL, "/v1") { + requestURL = strings.TrimPrefix(requestURL, "/v1") + } + return fmt.Sprintf("%s%s", baseURL, requestURL) } func GetAPIVersion(c *gin.Context) string { diff --git a/relay/constant/relay_mode.go b/relay/constant/relay_mode.go index 256715679213..e331f38ed442 100644 --- a/relay/constant/relay_mode.go +++ b/relay/constant/relay_mode.go @@ -54,7 +54,27 @@ const ( RelayModeResponsesCompact ) +func normalizeVersionPrefix(path string) string { + if len(path) < 3 || path[0] != '/' { + return path + } + secondSlash := strings.Index(path[1:], "/") + if secondSlash == -1 { + return path + } + secondSlash++ + prefix := path[:secondSlash] + if strings.HasPrefix(prefix, "/v") && len(prefix) > 2 { + verPart := prefix[2:] + if len(verPart) > 0 && verPart[0] >= '0' && verPart[0] <= '9' { + return "/v1" + path[secondSlash:] + } + } + return path +} + func Path2RelayMode(path string) int { + path = normalizeVersionPrefix(path) relayMode := RelayModeUnknown if strings.HasPrefix(path, "/v1/chat/completions") || strings.HasPrefix(path, "/pg/chat/completions") { relayMode = RelayModeChatCompletions