feat: support upstream model fetch for advanced custom channels (#5971)

* feat: support upstream model fetch for advanced custom channels

* fix: add advanced custom routes as separate groups

* fix: select advanced custom route entry before adding

---------

Co-authored-by: CaIon <i@caion.me>
This commit is contained in:
Seefs
2026-07-18 13:39:53 +08:00
committed by GitHub
co-authored by CaIon
parent 57746fc972
commit a6cf42c0f1
24 changed files with 1324 additions and 135 deletions
+135 -33
View File
@@ -16,6 +16,7 @@ import (
"github.com/QuantumNous/new-api/model"
relaychannel "github.com/QuantumNous/new-api/relay/channel"
"github.com/QuantumNous/new-api/relay/channel/ollama"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/service/authz"
@@ -201,22 +202,29 @@ func buildFetchModelsHeaders(channel *model.Channel, key string) (http.Header, e
headers = GetAuthHeader(key)
}
headerOverride := channel.GetHeaderOverride()
for k, v := range headerOverride {
if relaychannel.IsHeaderPassthroughRuleKey(k) {
continue
}
str, ok := v.(string)
if !ok {
return nil, fmt.Errorf("invalid header override for key %s", k)
}
if strings.Contains(str, "{api_key}") {
str = strings.ReplaceAll(str, "{api_key}", key)
}
headers.Set(k, str)
if err := applyFetchModelsHeaderOverrides(channel, key, headers); err != nil {
return nil, err
}
return headers, nil
}
func applyFetchModelsHeaderOverrides(channel *model.Channel, key string, headers http.Header) error {
info := &relaycommon.RelayInfo{
IsChannelTest: true,
ChannelMeta: &relaycommon.ChannelMeta{
ApiKey: key,
HeadersOverride: channel.GetHeaderOverride(),
},
}
overrides, err := relaychannel.ResolveHeaderOverride(info, nil)
if err != nil {
return err
}
for name, value := range overrides {
headers.Set(name, value)
}
return headers, nil
return nil
}
func FetchUpstreamModels(c *gin.Context) {
@@ -464,6 +472,10 @@ func validateTwoFactorAuth(twoFA *model.TwoFA, code string) bool {
// validateChannel 通用的渠道校验函数
func validateChannel(channel *model.Channel, isAdd bool) error {
if channel == nil {
return fmt.Errorf("channel cannot be empty")
}
// 校验 channel settings
if err := channel.ValidateSettings(); err != nil {
return fmt.Errorf("渠道额外设置[channel setting] 格式错误:%s", err.Error())
@@ -471,7 +483,7 @@ func validateChannel(channel *model.Channel, isAdd bool) error {
// 如果是添加操作,检查 channel 和 key 是否为空
if isAdd {
if channel == nil || channel.Key == "" {
if channel.Key == "" {
return fmt.Errorf("channel cannot be empty")
}
@@ -1155,13 +1167,87 @@ func equalStringPtr(a, b *string) bool {
return *a == *b
}
func FetchModels(c *gin.Context) {
var req struct {
BaseURL string `json:"base_url"`
Type int `json:"type"`
Key string `json:"key"`
type fetchModelsRequest struct {
ChannelID int `json:"channel_id"`
BaseURL *string `json:"base_url"`
Type int `json:"type"`
Key string `json:"key"`
AdvancedCustom *string `json:"advanced_custom"`
HeaderOverride *string `json:"header_override"`
Proxy *string `json:"proxy"`
}
func buildAdvancedCustomModelPreviewChannel(req fetchModelsRequest) (*model.Channel, error) {
var channel *model.Channel
if req.ChannelID > 0 {
savedChannel, err := model.GetChannelById(req.ChannelID, true)
if err != nil {
return nil, err
}
if savedChannel.Type != constant.ChannelTypeAdvancedCustom {
return nil, fmt.Errorf("channel %d is not an advanced custom channel", req.ChannelID)
}
channel = savedChannel
} else {
key := strings.TrimSpace(req.Key)
if key != "" {
key = strings.Split(key, "\n")[0]
}
channel = &model.Channel{
Type: req.Type,
Key: key,
}
}
if channel.Type != constant.ChannelTypeAdvancedCustom {
return nil, fmt.Errorf("channel type must be advanced custom")
}
if req.BaseURL != nil {
baseURL := strings.TrimSpace(*req.BaseURL)
channel.BaseURL = &baseURL
}
settings := channel.GetOtherSettings()
if req.AdvancedCustom != nil {
rawConfig := strings.TrimSpace(*req.AdvancedCustom)
if rawConfig == "" {
return nil, fmt.Errorf("advanced_custom is required")
}
var config dto.AdvancedCustomConfig
if err := common.UnmarshalJsonStr(rawConfig, &config); err != nil {
return nil, err
}
settings.AdvancedCustom = &config
} else if req.ChannelID <= 0 {
return nil, fmt.Errorf("advanced_custom is required")
}
channel.SetOtherSettings(settings)
if req.HeaderOverride != nil {
rawHeaderOverride := strings.TrimSpace(*req.HeaderOverride)
if rawHeaderOverride != "" {
var headerOverride map[string]any
if err := common.UnmarshalJsonStr(rawHeaderOverride, &headerOverride); err != nil {
return nil, fmt.Errorf("header_override must be a JSON object: %w", err)
}
}
channel.HeaderOverride = &rawHeaderOverride
}
if req.Proxy != nil {
channelSettings := channel.GetSetting()
channelSettings.Proxy = strings.TrimSpace(*req.Proxy)
channel.SetSetting(channelSettings)
}
if err := validateChannel(channel, false); err != nil {
return nil, err
}
return channel, nil
}
func FetchModels(c *gin.Context) {
var req fetchModelsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
@@ -1170,21 +1256,37 @@ func FetchModels(c *gin.Context) {
return
}
baseURL := req.BaseURL
if baseURL == "" {
baseURL = constant.ChannelBaseURLs[req.Type]
var channel *model.Channel
if req.Type == constant.ChannelTypeAdvancedCustom || req.ChannelID > 0 {
var err error
channel, err = buildAdvancedCustomModelPreviewChannel(req)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
} else {
baseURL := ""
if req.BaseURL != nil {
baseURL = strings.TrimSpace(*req.BaseURL)
}
if baseURL == "" {
baseURL = constant.ChannelBaseURLs[req.Type]
}
key := strings.TrimSpace(req.Key)
if req.Type != constant.ChannelTypeCodex {
key = strings.Split(key, "\n")[0]
}
channel = &model.Channel{
Type: req.Type,
Key: key,
BaseURL: &baseURL,
}
}
key := strings.TrimSpace(req.Key)
if req.Type != constant.ChannelTypeCodex {
key = strings.Split(key, "\n")[0]
}
channel := &model.Channel{
Type: req.Type,
Key: key,
BaseURL: &baseURL,
}
models, err := fetchChannelUpstreamModelIDs(channel)
if err != nil {
c.JSON(http.StatusOK, gin.H{