Files
sub2api/backend/internal/service/composite_route_resolver_test.go
T
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

268 lines
8.4 KiB
Go

package service
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
type compositeRouteRepoStub struct {
routes []CompositeModelRoute
}
func (s compositeRouteRepoStub) ListByGroup(ctx context.Context, groupID int64, includeDisabled bool) ([]CompositeModelRoute, error) {
routes := make([]CompositeModelRoute, 0, len(s.routes))
for _, route := range s.routes {
if route.GroupID != groupID {
continue
}
if !includeDisabled && !route.Enabled {
continue
}
routes = append(routes, route)
}
return routes, nil
}
func (s compositeRouteRepoStub) Create(ctx context.Context, route *CompositeModelRoute) error {
return nil
}
func (s compositeRouteRepoStub) Update(ctx context.Context, route *CompositeModelRoute) error {
return nil
}
func (s compositeRouteRepoStub) Delete(ctx context.Context, id int64) error {
return nil
}
func (s compositeRouteRepoStub) DeleteByGroup(ctx context.Context, groupID int64) error {
return nil
}
func TestCompositeRouteResolverExplicitExactRouteRewritesModel(t *testing.T) {
resolver := NewCompositeRouteResolver(compositeRouteRepoStub{
routes: []CompositeModelRoute{
{
ID: 10,
GroupID: 7,
PublicModel: "openrouter/gpt-5",
MatchType: CompositeRouteMatchExact,
TargetPlatform: PlatformOpenAI,
UpstreamModel: "gpt-5",
Endpoint: CompositeRouteEndpointAny,
Priority: 100,
Enabled: true,
},
},
})
decision, err := resolver.Resolve(context.Background(), 7, "openrouter/gpt-5", CompositeRouteEndpointChatCompletions)
require.NoError(t, err)
require.True(t, decision.Matched)
require.Equal(t, CompositeRouteSourceExplicit, decision.Source)
require.Equal(t, PlatformOpenAI, decision.TargetPlatform)
require.Equal(t, "gpt-5", decision.UpstreamModel)
require.NotNil(t, decision.Route)
require.Equal(t, int64(10), decision.Route.ID)
}
func TestCompositeRouteResolverPrefersEndpointSpecificLongestPrefix(t *testing.T) {
resolver := NewCompositeRouteResolver(compositeRouteRepoStub{
routes: []CompositeModelRoute{
{
ID: 1,
GroupID: 7,
PublicModel: "router/",
MatchType: CompositeRouteMatchPrefix,
TargetPlatform: PlatformAnthropic,
Endpoint: CompositeRouteEndpointAny,
Priority: 10,
Enabled: true,
},
{
ID: 2,
GroupID: 7,
PublicModel: "router/gpt-",
MatchType: CompositeRouteMatchPrefix,
TargetPlatform: PlatformOpenAI,
UpstreamModel: "gpt-family",
Endpoint: CompositeRouteEndpointResponses,
Priority: 100,
Enabled: true,
},
},
})
decision, err := resolver.Resolve(context.Background(), 7, "router/gpt-5", CompositeRouteEndpointResponses)
require.NoError(t, err)
require.True(t, decision.Matched)
require.Equal(t, CompositeRouteSourceExplicit, decision.Source)
require.Equal(t, PlatformOpenAI, decision.TargetPlatform)
require.Equal(t, "gpt-family", decision.UpstreamModel)
require.NotNil(t, decision.Route)
require.Equal(t, int64(2), decision.Route.ID)
}
// TestCompositeRouteResolverPrefixEmptyUpstreamPassesThroughRequestedModel 验证:
// 前缀匹配路由留空 upstream_model 时,转发的是具体请求模型(各自原样),而不是
// 塌缩成 public_model。这是「留空 = 透传原始模型」语义的核心场景。
func TestCompositeRouteResolverPrefixEmptyUpstreamPassesThroughRequestedModel(t *testing.T) {
resolver := NewCompositeRouteResolver(compositeRouteRepoStub{
routes: []CompositeModelRoute{
{
ID: 1,
GroupID: 7,
PublicModel: "deepseek-v4",
MatchType: CompositeRouteMatchPrefix,
TargetPlatform: PlatformOpenAI,
UpstreamModel: "", // 留空 = 透传
Endpoint: CompositeRouteEndpointAny,
Priority: 100,
Enabled: true,
},
},
})
for _, model := range []string{"deepseek-v4-flash", "deepseek-v4-pro", "deepseek-v4"} {
decision, err := resolver.Resolve(context.Background(), 7, model, CompositeRouteEndpointChatCompletions)
require.NoError(t, err)
require.True(t, decision.Matched, "model %q should match prefix route", model)
require.Equal(t, CompositeRouteSourceExplicit, decision.Source)
require.Equal(t, PlatformOpenAI, decision.TargetPlatform)
require.Equal(t, model, decision.UpstreamModel, "model %q should pass through verbatim", model)
}
}
// TestCompositeRouteResolverPrefixExplicitUpstreamStillFixed 验证:前缀匹配路由显式
// 填写 upstream_model 时,所有命中请求仍转发同一个固定上游模型(行为不变)。
func TestCompositeRouteResolverPrefixExplicitUpstreamStillFixed(t *testing.T) {
resolver := NewCompositeRouteResolver(compositeRouteRepoStub{
routes: []CompositeModelRoute{
{
ID: 1,
GroupID: 7,
PublicModel: "deepseek-v4",
MatchType: CompositeRouteMatchPrefix,
TargetPlatform: PlatformOpenAI,
UpstreamModel: "deepseek-chat",
Endpoint: CompositeRouteEndpointAny,
Priority: 100,
Enabled: true,
},
},
})
for _, model := range []string{"deepseek-v4-flash", "deepseek-v4-pro"} {
decision, err := resolver.Resolve(context.Background(), 7, model, CompositeRouteEndpointChatCompletions)
require.NoError(t, err)
require.True(t, decision.Matched)
require.Equal(t, "deepseek-chat", decision.UpstreamModel)
}
}
func TestCompositeRouteResolverIgnoresDisabledRoutesAndFallsBackToDetector(t *testing.T) {
resolver := NewCompositeRouteResolver(compositeRouteRepoStub{
routes: []CompositeModelRoute{
{
ID: 1,
GroupID: 7,
PublicModel: "gpt-5",
MatchType: CompositeRouteMatchExact,
TargetPlatform: PlatformAnthropic,
UpstreamModel: "claude-sonnet-4-6",
Endpoint: CompositeRouteEndpointAny,
Priority: 100,
Enabled: false,
},
},
})
decision, err := resolver.Resolve(context.Background(), 7, "gpt-5", CompositeRouteEndpointAny)
require.NoError(t, err)
require.True(t, decision.Matched)
require.Equal(t, CompositeRouteSourceDetector, decision.Source)
require.Equal(t, PlatformOpenAI, decision.TargetPlatform)
require.Equal(t, "gpt-5", decision.UpstreamModel)
require.Nil(t, decision.Route)
}
func TestCompositeRouteResolverExplicitRoutesCoverBucketTwoProviders(t *testing.T) {
resolver := NewCompositeRouteResolver(compositeRouteRepoStub{
routes: []CompositeModelRoute{
{
ID: 1,
GroupID: 7,
PublicModel: "all/gpt-5",
MatchType: CompositeRouteMatchExact,
TargetPlatform: PlatformOpenAI,
UpstreamModel: "gpt-5",
Endpoint: CompositeRouteEndpointResponses,
Priority: 100,
Enabled: true,
},
{
ID: 2,
GroupID: 7,
PublicModel: "all/claude-sonnet",
MatchType: CompositeRouteMatchExact,
TargetPlatform: PlatformAnthropic,
UpstreamModel: "claude-sonnet-4-6",
Endpoint: CompositeRouteEndpointMessages,
Priority: 100,
Enabled: true,
},
{
ID: 3,
GroupID: 7,
PublicModel: "all/gemini-pro",
MatchType: CompositeRouteMatchExact,
TargetPlatform: PlatformGemini,
UpstreamModel: "gemini-2.5-pro",
Endpoint: CompositeRouteEndpointGemini,
Priority: 100,
Enabled: true,
},
{
ID: 4,
GroupID: 7,
PublicModel: "all/grok",
MatchType: CompositeRouteMatchExact,
TargetPlatform: PlatformGrok,
UpstreamModel: "grok-4.3",
Endpoint: CompositeRouteEndpointResponses,
Priority: 100,
Enabled: true,
},
},
})
tests := []struct {
model string
endpoint string
wantPlatform string
wantUpstream string
}{
{"all/gpt-5", CompositeRouteEndpointResponses, PlatformOpenAI, "gpt-5"},
{"all/claude-sonnet", CompositeRouteEndpointMessages, PlatformAnthropic, "claude-sonnet-4-6"},
{"all/gemini-pro", CompositeRouteEndpointGemini, PlatformGemini, "gemini-2.5-pro"},
{"all/grok", CompositeRouteEndpointResponses, PlatformGrok, "grok-4.3"},
}
for _, tt := range tests {
t.Run(tt.model, func(t *testing.T) {
decision, err := resolver.Resolve(context.Background(), 7, tt.model, tt.endpoint)
require.NoError(t, err)
require.True(t, decision.Matched)
require.Equal(t, CompositeRouteSourceExplicit, decision.Source)
require.Equal(t, tt.wantPlatform, decision.TargetPlatform)
require.Equal(t, tt.wantUpstream, decision.UpstreamModel)
})
}
}