Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions controller/channel-billing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion controller/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/QuantumNous/new-api/i18n"
"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"
Expand Down Expand Up @@ -1220,7 +1221,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 {
Expand Down
9 changes: 5 additions & 4 deletions controller/channel_upstream_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -297,18 +298,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()
Expand Down
3 changes: 2 additions & 1 deletion controller/video_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -112,7 +113,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:
Comment on lines 115 to 118

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reuse task-captured auth for OpenAI/Sora content fetches

Line 112 still uses channel.Key for the follow-up request. If channel keys rotate (or multi-key selection changes) after submission, completed tasks can fail to download with upstream auth errors. Prefer task.PrivateData.Key first (fallback to channel.Key), and apply stored resolved header overrides when available.

💡 Suggested fix (key selection)
 case constant.ChannelTypeOpenAI, constant.ChannelTypeSora:
 	videoURL = relaycommon.GetFullRequestURL(baseURL, fmt.Sprintf("/v1/videos/%s/content", task.GetUpstreamTaskID()), channel.Type)
-	req.Header.Set("Authorization", "Bearer "+channel.Key)
+	apiKey := strings.TrimSpace(task.PrivateData.Key)
+	if apiKey == "" {
+		apiKey = strings.TrimSpace(channel.Key)
+	}
+	req.Header.Set("Authorization", "Bearer "+apiKey)

Based on learnings, async video follow-up requests should reuse authentication context captured at submission time (including task-stored key and resolved header overrides).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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:
case constant.ChannelTypeOpenAI, constant.ChannelTypeSora:
videoURL = relaycommon.GetFullRequestURL(baseURL, fmt.Sprintf("/v1/videos/%s/content", task.GetUpstreamTaskID()), channel.Type)
apiKey := strings.TrimSpace(task.PrivateData.Key)
if apiKey == "" {
apiKey = strings.TrimSpace(channel.Key)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
default:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/video_proxy.go` around lines 110 - 113, The follow-up fetch for
OpenAI/Sora videos uses the live channel.Key which can break if keys rotate;
update the logic in the case handling (where relaycommon.GetFullRequestURL and
task.GetUpstreamTaskID are used) to prefer the submission-captured key stored on
the task (task.PrivateData.Key) falling back to channel.Key, and apply any
stored resolved header overrides from task.PrivateData (e.g., Authorization or
other headers) to req.Header before sending the request so the request reuses
the exact auth context captured at submission time.

Source: Learnings

// Video URL is stored in PrivateData.ResultURL (fallback to FailReason for old data)
Expand Down
2 changes: 1 addition & 1 deletion relay/channel/deepseek/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Expand Down
10 changes: 5 additions & 5 deletions relay/channel/moonshot/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,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
}
}

Expand Down
5 changes: 4 additions & 1 deletion relay/channel/openai/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,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")
Expand Down Expand Up @@ -174,6 +174,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
Expand Down
2 changes: 1 addition & 1 deletion relay/channel/perplexity/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion relay/channel/siliconflow/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
48 changes: 43 additions & 5 deletions relay/common/relay_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,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:]
}
Comment thread
waxiangzi marked this conversation as resolved.
}
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 SanitizeURLForLog(rawURL string) string {
Expand Down
20 changes: 20 additions & 0 deletions relay/constant/relay_mode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down