| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376 |
- package service
- import (
- "context"
- "errors"
- "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)
- }
- // Scenario: 唯一平台的精确别名可路由
- func TestCompositeRouteResolverUsesAccountModelOwnershipForUnprefixedAlias(t *testing.T) {
- resolver := NewCompositeRouteResolver(nil)
- resolver.SetModelOwnershipResolver(func(_ context.Context, groupID int64, model string) (CompositeModelOwnership, error) {
- require.Equal(t, int64(7), groupID)
- require.Equal(t, "reasoning-alias", model)
- return CompositeModelOwnership{TargetPlatform: PlatformDeepseek, Matched: true}, nil
- })
- decision, err := resolver.Resolve(context.Background(), 7, "reasoning-alias", CompositeRouteEndpointChatCompletions)
- require.NoError(t, err)
- require.True(t, decision.Matched)
- require.Equal(t, CompositeRouteSourceAccount, decision.Source)
- require.Equal(t, PlatformDeepseek, decision.TargetPlatform)
- require.Equal(t, "reasoning-alias", decision.UpstreamModel)
- }
- func TestCompositeRouteResolverAccountOwnershipOverridesBuiltInDetector(t *testing.T) {
- resolver := NewCompositeRouteResolver(nil)
- resolver.SetModelOwnershipResolver(func(context.Context, int64, string) (CompositeModelOwnership, error) {
- return CompositeModelOwnership{TargetPlatform: PlatformDeepseek, Matched: true}, nil
- })
- decision, err := resolver.Resolve(context.Background(), 7, "gpt-5", CompositeRouteEndpointResponses)
- require.NoError(t, err)
- require.True(t, decision.Matched)
- require.Equal(t, CompositeRouteSourceAccount, decision.Source)
- require.Equal(t, PlatformDeepseek, decision.TargetPlatform)
- }
- // Scenario: 显式路由保持最高优先级
- func TestCompositeRouteResolverExplicitRouteBeatsAccountOwnership(t *testing.T) {
- resolver := NewCompositeRouteResolver(compositeRouteRepoStub{
- routes: []CompositeModelRoute{{
- ID: 10,
- GroupID: 7,
- PublicModel: "reasoning-alias",
- MatchType: CompositeRouteMatchExact,
- TargetPlatform: PlatformOpenAI,
- UpstreamModel: "gpt-5",
- Endpoint: CompositeRouteEndpointAny,
- Enabled: true,
- }},
- })
- resolver.SetModelOwnershipResolver(func(context.Context, int64, string) (CompositeModelOwnership, error) {
- return CompositeModelOwnership{TargetPlatform: PlatformDeepseek, Matched: true}, nil
- })
- decision, err := resolver.Resolve(context.Background(), 7, "reasoning-alias", 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)
- }
- // Scenario: 跨平台同名别名不被猜测
- func TestCompositeRouteResolverDoesNotGuessAmbiguousAccountOwnership(t *testing.T) {
- resolver := NewCompositeRouteResolver(nil)
- resolver.SetModelOwnershipResolver(func(context.Context, int64, string) (CompositeModelOwnership, error) {
- return CompositeModelOwnership{Ambiguous: true}, nil
- })
- decision, err := resolver.Resolve(context.Background(), 7, "shared-alias", CompositeRouteEndpointChatCompletions)
- require.NoError(t, err)
- require.False(t, decision.Matched)
- require.Empty(t, decision.TargetPlatform)
- require.Equal(t, "model is exposed by multiple provider platforms", decision.Reason)
- }
- func TestCompositeRouteResolverOwnershipLookupErrorFallsBackOnlyForDetectableModels(t *testing.T) {
- lookupErr := errors.New("account catalog unavailable")
- resolver := NewCompositeRouteResolver(nil)
- resolver.SetModelOwnershipResolver(func(context.Context, int64, string) (CompositeModelOwnership, error) {
- return CompositeModelOwnership{}, lookupErr
- })
- detected, err := resolver.Resolve(context.Background(), 7, "gpt-5", CompositeRouteEndpointResponses)
- require.NoError(t, err)
- require.True(t, detected.Matched)
- require.Equal(t, CompositeRouteSourceDetector, detected.Source)
- require.Equal(t, PlatformOpenAI, detected.TargetPlatform)
- unknown, err := resolver.Resolve(context.Background(), 7, "company-model", CompositeRouteEndpointResponses)
- require.ErrorIs(t, err, lookupErr)
- require.False(t, unknown.Matched)
- }
- 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 TestCompositeRouteResolverDetectsKimiCodeBareModels(t *testing.T) {
- resolver := NewCompositeRouteResolver(nil)
- for _, model := range []string{"k3", "k3-256k", "kimi-code/k3"} {
- t.Run(model, func(t *testing.T) {
- decision, err := resolver.Resolve(context.Background(), 7, model, CompositeRouteEndpointMessages)
- require.NoError(t, err)
- require.True(t, decision.Matched)
- require.Equal(t, CompositeRouteSourceDetector, decision.Source)
- require.Equal(t, PlatformKimi, decision.TargetPlatform)
- require.Equal(t, model, decision.UpstreamModel)
- })
- }
- }
- 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)
- })
- }
- }
|