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
+119 -5
View File
@@ -2,8 +2,11 @@ package controller
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"slices"
"strings"
@@ -14,9 +17,13 @@ import (
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/relay/channel/advancedcustom"
"github.com/QuantumNous/new-api/relay/channel/gemini"
"github.com/QuantumNous/new-api/relay/channel/ollama"
relaycommon "github.com/QuantumNous/new-api/relay/common"
relayconstant "github.com/QuantumNous/new-api/relay/constant"
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
@@ -255,6 +262,76 @@ func getUpstreamModelUpdateMinCheckIntervalSeconds() int64 {
return interval
}
func parseOpenAIModelIDs(body []byte) ([]string, error) {
var result struct {
Data *[]OpenAIModel `json:"data"`
}
if err := common.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("invalid OpenAI Models response: %w", err)
}
if result.Data == nil {
return nil, fmt.Errorf("invalid OpenAI Models response: data is required")
}
ids := normalizeModelNames(lo.Map(*result.Data, func(item OpenAIModel, _ int) string {
return item.ID
}))
if len(ids) == 0 {
return nil, fmt.Errorf("OpenAI Models response contains no valid model IDs")
}
return ids, nil
}
func sanitizeFetchModelsError(err error, key string) error {
if err == nil {
return nil
}
// net/http includes the complete request URL in url.Error. Discovery routes
// may put the API key in a custom query name or value, so never return that
// wrapper to an API client.
var urlErr *url.Error
if errors.As(err, &urlErr) && urlErr.Err != nil {
err = urlErr.Err
}
message := err.Error()
key = strings.TrimSpace(key)
if key != "" {
message = strings.ReplaceAll(message, key, "[REDACTED]")
message = strings.ReplaceAll(message, url.QueryEscape(key), "[REDACTED]")
message = strings.ReplaceAll(message, url.PathEscape(key), "[REDACTED]")
}
return errors.New(message)
}
func getFetchModelsResponseBody(method string, requestURL string, channel *model.Channel, headers http.Header) ([]byte, error) {
request, err := http.NewRequest(method, requestURL, nil)
if err != nil {
return nil, err
}
for name, values := range headers {
for _, value := range values {
request.Header.Add(name, value)
}
if strings.EqualFold(name, "Host") {
request.Host = headers.Get(name)
}
}
client, err := service.NewProxyHttpClient(channel.GetSetting().Proxy)
if err != nil {
return nil, err
}
response, err := client.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("status code: %d", response.StatusCode)
}
return io.ReadAll(response.Body)
}
func fetchChannelUpstreamModelIDs(channel *model.Channel) ([]string, error) {
baseURL := constant.ChannelBaseURLs[channel.Type]
if channel.GetBaseURL() != "" {
@@ -285,6 +362,10 @@ func fetchChannelUpstreamModelIDs(channel *model.Channel) ([]string, error) {
return normalizeModelNames(models), nil
}
if channel.Type == constant.ChannelTypeAdvancedCustom {
return fetchAdvancedCustomUpstreamModelIDs(channel, baseURL)
}
if channel.Type == constant.ChannelTypeCodex {
return service.FetchCodexChannelModels(channel)
}
@@ -323,29 +404,62 @@ func fetchChannelUpstreamModelIDs(channel *model.Channel) ([]string, error) {
headers, err := buildFetchModelsHeaders(channel, key)
if err != nil {
return nil, err
return nil, sanitizeFetchModelsError(err, key)
}
body, err := GetResponseBody(http.MethodGet, url, channel, headers)
body, err := getFetchModelsResponseBody(http.MethodGet, url, channel, headers)
if err != nil {
return nil, err
return nil, sanitizeFetchModelsError(err, key)
}
var result OpenAIModelsResponse
if err := common.Unmarshal(body, &result); err != nil {
return nil, err
}
ids := lo.Map(result.Data, func(item OpenAIModel, _ int) string {
if channel.Type == constant.ChannelTypeGemini {
return strings.TrimPrefix(item.ID, "models/")
}
return item.ID
})
return normalizeModelNames(ids), nil
}
func fetchAdvancedCustomUpstreamModelIDs(channel *model.Channel, baseURL string) ([]string, error) {
key, _, apiErr := channel.GetNextEnabledKey()
if apiErr != nil {
return nil, fmt.Errorf("获取渠道密钥失败: %w", apiErr)
}
key = strings.TrimSpace(key)
info := &relaycommon.RelayInfo{
RelayFormat: types.RelayFormatOpenAI,
RelayMode: relayconstant.RelayModeUnknown,
RequestURLPath: dto.AdvancedCustomModelListPath,
ChannelMeta: &relaycommon.ChannelMeta{
ChannelType: constant.ChannelTypeAdvancedCustom,
ChannelBaseUrl: baseURL,
ApiKey: key,
ChannelOtherSettings: channel.GetOtherSettings(),
},
}
adaptor := &advancedcustom.Adaptor{}
url, headers, err := adaptor.BuildModelListRequest(info)
if err != nil {
return nil, sanitizeFetchModelsError(err, key)
}
if err := applyFetchModelsHeaderOverrides(channel, key, headers); err != nil {
return nil, sanitizeFetchModelsError(err, key)
}
body, err := getFetchModelsResponseBody(http.MethodGet, url, channel, headers)
if err != nil {
return nil, sanitizeFetchModelsError(err, key)
}
return parseOpenAIModelIDs(body)
}
func updateChannelUpstreamModelSettings(channel *model.Channel, settings dto.ChannelOtherSettings, updateModels bool) error {
channel.SetOtherSettings(settings)
updates := map[string]interface{}{