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:
@@ -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{}{
|
||||
|
||||
Reference in New Issue
Block a user