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:
@@ -193,6 +193,51 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
return a.routeURL(info)
|
||||
}
|
||||
|
||||
func (a *Adaptor) BuildModelListRequest(info *relaycommon.RelayInfo) (string, http.Header, error) {
|
||||
if info == nil {
|
||||
return "", nil, errors.New("missing relay info")
|
||||
}
|
||||
config := info.ChannelOtherSettings.AdvancedCustom
|
||||
if config == nil {
|
||||
return "", nil, errors.New("advanced_custom is required")
|
||||
}
|
||||
if err := config.Validate(); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
route, ok := config.ModelListRoute()
|
||||
if !ok {
|
||||
return "", nil, errors.New("advanced custom channel does not configure a /v1/models route")
|
||||
}
|
||||
converter := strings.TrimSpace(route.Converter)
|
||||
if converter == "" {
|
||||
converter = relayconvert.ConverterNone
|
||||
}
|
||||
if converter != relayconvert.ConverterNone {
|
||||
return "", nil, fmt.Errorf("converter %q does not support model list requests", converter)
|
||||
}
|
||||
|
||||
requestURL, err := buildRouteURL(route, converter, info)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
header := http.Header{}
|
||||
auth := route.Auth
|
||||
if auth == nil {
|
||||
header.Set("Authorization", "Bearer "+info.ApiKey)
|
||||
return requestURL, header, nil
|
||||
}
|
||||
|
||||
switch strings.TrimSpace(auth.Type) {
|
||||
case dto.AdvancedCustomAuthTypeNone, dto.AdvancedCustomAuthTypeQuery:
|
||||
case dto.AdvancedCustomAuthTypeHeader:
|
||||
header.Set(strings.TrimSpace(auth.Name), applyAuthTemplate(auth.Value, info.ApiKey))
|
||||
default:
|
||||
return "", nil, fmt.Errorf("invalid advanced custom auth type: %s", auth.Type)
|
||||
}
|
||||
return requestURL, header, nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) SetupRequestHeader(c *gin.Context, header *http.Header, info *relaycommon.RelayInfo) error {
|
||||
if err := a.resolve(c, info); err != nil {
|
||||
return err
|
||||
@@ -343,11 +388,15 @@ func incomingRequestPath(c *gin.Context, info *relaycommon.RelayInfo) string {
|
||||
}
|
||||
|
||||
func (a *Adaptor) routeURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
parsedURL, err := resolveUpstreamTargetURL(applyUpstreamPathTemplate(strings.TrimSpace(a.route.UpstreamPath), info), info)
|
||||
return buildRouteURL(a.route, a.converter, info)
|
||||
}
|
||||
|
||||
func buildRouteURL(route dto.AdvancedCustomRoute, converter string, info *relaycommon.RelayInfo) (string, error) {
|
||||
parsedURL, err := resolveUpstreamTargetURL(applyUpstreamPathTemplate(strings.TrimSpace(route.UpstreamPath), info), info)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if shouldUseGeminiStreamURL(a.converter, info) {
|
||||
if shouldUseGeminiStreamURL(converter, info) {
|
||||
useGeminiStreamGenerateContentURL(parsedURL)
|
||||
}
|
||||
if info != nil && info.RelayMode == relayconstant.RelayModeRealtime {
|
||||
@@ -358,9 +407,9 @@ func (a *Adaptor) routeURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
parsedURL.Scheme = "ws"
|
||||
}
|
||||
}
|
||||
if a.route.Auth != nil && strings.TrimSpace(a.route.Auth.Type) == dto.AdvancedCustomAuthTypeQuery {
|
||||
if route.Auth != nil && strings.TrimSpace(route.Auth.Type) == dto.AdvancedCustomAuthTypeQuery {
|
||||
query := parsedURL.Query()
|
||||
query.Set(strings.TrimSpace(a.route.Auth.Name), applyAuthTemplate(a.route.Auth.Value, info.ApiKey))
|
||||
query.Set(strings.TrimSpace(route.Auth.Name), applyAuthTemplate(route.Auth.Value, info.ApiKey))
|
||||
parsedURL.RawQuery = query.Encode()
|
||||
}
|
||||
return parsedURL.String(), nil
|
||||
|
||||
@@ -284,6 +284,144 @@ func TestAdaptorMatchesGeminiIncomingPathTemplate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdaptorBuildModelListRequestUsesConfiguredRouteAuth(t *testing.T) {
|
||||
adaptor := &Adaptor{}
|
||||
info := advancedCustomRelayInfo(&dto.AdvancedCustomConfig{
|
||||
Routes: []dto.AdvancedCustomRoute{
|
||||
{
|
||||
IncomingPath: "/v1/models",
|
||||
UpstreamPath: "/provider/models",
|
||||
Converter: relayconvert.ConverterNone,
|
||||
Auth: &dto.AdvancedCustomRouteAuth{
|
||||
Type: dto.AdvancedCustomAuthTypeHeader,
|
||||
Name: "x-api-key",
|
||||
Value: "token {api_key}",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
info.RequestURLPath = "/v1/models"
|
||||
|
||||
requestURL, header, err := adaptor.BuildModelListRequest(info)
|
||||
require.NoError(t, err)
|
||||
|
||||
parsedURL, err := url.Parse(requestURL)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "fallback.example", parsedURL.Host)
|
||||
assert.Equal(t, "/provider/models", parsedURL.Path)
|
||||
assert.Equal(t, "token sk-test", header.Get("x-api-key"))
|
||||
assert.Empty(t, header.Get("Authorization"))
|
||||
}
|
||||
|
||||
func TestAdaptorBuildModelListRequestUsesConfiguredQueryAuth(t *testing.T) {
|
||||
adaptor := &Adaptor{}
|
||||
info := advancedCustomRelayInfo(&dto.AdvancedCustomConfig{
|
||||
Routes: []dto.AdvancedCustomRoute{
|
||||
{
|
||||
IncomingPath: "/v1/models",
|
||||
UpstreamPath: "https://upstream.example/v1/models?existing=1",
|
||||
Converter: relayconvert.ConverterNone,
|
||||
Auth: &dto.AdvancedCustomRouteAuth{
|
||||
Type: dto.AdvancedCustomAuthTypeQuery,
|
||||
Name: "key",
|
||||
Value: "{api_key}",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
info.RequestURLPath = "/v1/models"
|
||||
|
||||
requestURL, header, err := adaptor.BuildModelListRequest(info)
|
||||
require.NoError(t, err)
|
||||
|
||||
parsedURL, err := url.Parse(requestURL)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "upstream.example", parsedURL.Host)
|
||||
assert.Equal(t, "/v1/models", parsedURL.Path)
|
||||
assert.Equal(t, "1", parsedURL.Query().Get("existing"))
|
||||
assert.Equal(t, "sk-test", parsedURL.Query().Get("key"))
|
||||
assert.Empty(t, header.Get("Authorization"))
|
||||
}
|
||||
|
||||
func TestAdaptorBuildModelListRequestDefaultAndNoAuth(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
auth *dto.AdvancedCustomRouteAuth
|
||||
wantAuthorization string
|
||||
}{
|
||||
{
|
||||
name: "default bearer",
|
||||
wantAuthorization: "Bearer sk-test",
|
||||
},
|
||||
{
|
||||
name: "no authentication",
|
||||
auth: &dto.AdvancedCustomRouteAuth{
|
||||
Type: dto.AdvancedCustomAuthTypeNone,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
info := advancedCustomRelayInfo(&dto.AdvancedCustomConfig{
|
||||
Routes: []dto.AdvancedCustomRoute{
|
||||
{
|
||||
IncomingPath: dto.AdvancedCustomModelListPath,
|
||||
UpstreamPath: "/provider/models",
|
||||
Auth: tt.auth,
|
||||
},
|
||||
},
|
||||
})
|
||||
info.RequestURLPath = "/unrelated/path"
|
||||
|
||||
requestURL, header, err := (&Adaptor{}).BuildModelListRequest(info)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "https://fallback.example/provider/models", requestURL)
|
||||
assert.Equal(t, tt.wantAuthorization, header.Get("Authorization"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdaptorBuildModelListRequestDoesNotReuseRelayRoute(t *testing.T) {
|
||||
adaptor := &Adaptor{}
|
||||
info := advancedCustomRelayInfo(&dto.AdvancedCustomConfig{
|
||||
Routes: []dto.AdvancedCustomRoute{
|
||||
{
|
||||
IncomingPath: "/v1/chat/completions",
|
||||
UpstreamPath: "/chat",
|
||||
},
|
||||
{
|
||||
IncomingPath: dto.AdvancedCustomModelListPath,
|
||||
UpstreamPath: "/provider/models",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
chatURL, err := adaptor.GetRequestURL(info)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "https://fallback.example/chat", chatURL)
|
||||
|
||||
modelURL, header, err := adaptor.BuildModelListRequest(info)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "https://fallback.example/provider/models", modelURL)
|
||||
assert.Equal(t, "Bearer sk-test", header.Get("Authorization"))
|
||||
}
|
||||
|
||||
func TestAdaptorBuildModelListRequestRequiresConfiguredRoute(t *testing.T) {
|
||||
info := advancedCustomRelayInfo(&dto.AdvancedCustomConfig{
|
||||
Routes: []dto.AdvancedCustomRoute{
|
||||
{
|
||||
IncomingPath: "/v1/chat/completions",
|
||||
UpstreamPath: "/v1/chat/completions",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
_, _, err := (&Adaptor{}).BuildModelListRequest(info)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "does not configure a /v1/models route")
|
||||
}
|
||||
|
||||
func TestAdaptorConvertsResponsesRequestToOpenAIChatUpstream(t *testing.T) {
|
||||
adaptor := &Adaptor{}
|
||||
info := advancedCustomRelayInfo(&dto.AdvancedCustomConfig{
|
||||
|
||||
Reference in New Issue
Block a user