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
+33
View File
@@ -112,6 +112,9 @@ const (
advancedCustomEndpointPathEmbeddings = "/v1/embeddings"
)
// AdvancedCustomModelListPath identifies the optional OpenAI Models discovery route.
const AdvancedCustomModelListPath = "/v1/models"
// MatchPath returns the first route whose IncomingPath matches requestPath.
// Matching mirrors the relay adaptor: exact match, {model} placeholder, and
// :generateContent <-> :streamGenerateContent equivalence.
@@ -143,6 +146,20 @@ func (c *AdvancedCustomConfig) MatchPathForModel(requestPath string, model strin
return AdvancedCustomRoute{}, false
}
// ModelListRoute returns the explicitly configured OpenAI Models discovery route.
// Template routes that merely happen to match /v1/models are not discovery routes.
func (c *AdvancedCustomConfig) ModelListRoute() (AdvancedCustomRoute, bool) {
if c == nil {
return AdvancedCustomRoute{}, false
}
for _, route := range c.Routes {
if strings.TrimSpace(route.IncomingPath) == AdvancedCustomModelListPath {
return route, true
}
}
return AdvancedCustomRoute{}, false
}
// SupportsPath reports whether any route matches requestPath.
func (c *AdvancedCustomConfig) SupportsPath(requestPath string) bool {
_, ok := c.MatchPath(requestPath)
@@ -307,6 +324,7 @@ func (c *AdvancedCustomConfig) Validate() error {
}
paths := make(map[string]*advancedCustomPathModelState, len(c.Routes))
modelListRouteIndex := -1
for i := range c.Routes {
route := c.Routes[i]
route.IncomingPath = strings.TrimSpace(route.IncomingPath)
@@ -325,6 +343,21 @@ func (c *AdvancedCustomConfig) Validate() error {
if strings.Contains(route.IncomingPath, "?") {
return fmt.Errorf("advanced_custom.advanced_routes[%d].incoming_path must not include query", i)
}
if route.IncomingPath == AdvancedCustomModelListPath {
if modelListRouteIndex >= 0 {
return fmt.Errorf("advanced_custom.advanced_routes[%d] duplicates the /v1/models route at advanced_routes[%d]", i, modelListRouteIndex)
}
modelListRouteIndex = i
if len(normalizeAdvancedCustomRouteModels(route.Models)) > 0 {
return fmt.Errorf("advanced_custom.advanced_routes[%d].models must be empty for /v1/models", i)
}
if route.Converter != advancedCustomConverterNone {
return fmt.Errorf("advanced_custom.advanced_routes[%d].converter must be none for /v1/models", i)
}
if strings.Contains(upstreamPath, advancedCustomModelPlaceholder) {
return fmt.Errorf("advanced_custom.advanced_routes[%d].upstream_path must not contain %s for /v1/models", i, advancedCustomModelPlaceholder)
}
}
if err := validateAdvancedCustomRouteModels(i, route.IncomingPath, route.Models, paths); err != nil {
return err
}
+88
View File
@@ -58,6 +58,94 @@ func TestAdvancedCustomValidateResponsesToChatConverterPath(t *testing.T) {
}
}
func TestAdvancedCustomValidateModelListRouteConstraints(t *testing.T) {
valid := &AdvancedCustomConfig{
Routes: []AdvancedCustomRoute{
{
IncomingPath: AdvancedCustomModelListPath,
UpstreamPath: "https://upstream.example/custom/models",
Converter: advancedCustomConverterNone,
},
},
}
require.NoError(t, valid.Validate())
tests := []struct {
name string
routes []AdvancedCustomRoute
want string
}{
{
name: "model matching rules",
routes: []AdvancedCustomRoute{
{
IncomingPath: AdvancedCustomModelListPath,
UpstreamPath: "/v1/models",
Models: []string{"gpt-4o"},
},
},
want: "models must be empty",
},
{
name: "converter",
routes: []AdvancedCustomRoute{
{
IncomingPath: AdvancedCustomModelListPath,
UpstreamPath: "/v1/models",
Converter: advancedCustomConverterOpenAIChatToOpenAIResponses,
},
},
want: "converter must be none",
},
{
name: "model placeholder",
routes: []AdvancedCustomRoute{
{
IncomingPath: AdvancedCustomModelListPath,
UpstreamPath: "/v1/models/{model}",
},
},
want: "upstream_path must not contain {model}",
},
{
name: "duplicate routes",
routes: []AdvancedCustomRoute{
{IncomingPath: AdvancedCustomModelListPath, UpstreamPath: "/v1/models"},
{IncomingPath: AdvancedCustomModelListPath, UpstreamPath: "/provider/models"},
},
want: "duplicates the /v1/models route",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := (&AdvancedCustomConfig{Routes: tt.routes}).Validate()
require.Error(t, err)
assert.Contains(t, err.Error(), tt.want)
})
}
}
func TestAdvancedCustomModelListRouteRequiresExactIncomingPath(t *testing.T) {
config := &AdvancedCustomConfig{
Routes: []AdvancedCustomRoute{
{
IncomingPath: "/v1/{model}",
UpstreamPath: "/generic/{model}",
},
{
IncomingPath: AdvancedCustomModelListPath,
UpstreamPath: "/provider/models",
},
},
}
require.NoError(t, config.Validate())
route, ok := config.ModelListRoute()
require.True(t, ok)
assert.Equal(t, "/provider/models", route.UpstreamPath)
}
func TestAdvancedCustomValidateDuplicateIncomingPathWithDisjointModels(t *testing.T) {
config := &AdvancedCustomConfig{
Routes: []AdvancedCustomRoute{