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