package dto import ( "regexp" "testing" "github.com/QuantumNous/new-api/constant" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestAdvancedCustomValidateResponsesToChatConverterPath(t *testing.T) { valid := &AdvancedCustomConfig{ Routes: []AdvancedCustomRoute{ { IncomingPath: "/v1/responses", UpstreamPath: "/v1/chat/completions", Converter: advancedCustomConverterOpenAIResponsesToOpenAIChat, }, }, } require.NoError(t, valid.Validate()) validGemini := &AdvancedCustomConfig{ Routes: []AdvancedCustomRoute{ { IncomingPath: "/v1/responses", UpstreamPath: "/v1beta/models/{model}:generateContent", Converter: advancedCustomConverterOpenAIResponsesToGemini, }, }, } require.NoError(t, validGemini.Validate()) tests := []struct { name string incomingPath string }{ {name: "chat completions", incomingPath: "/v1/chat/completions"}, {name: "responses compact", incomingPath: "/v1/responses/compact"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { config := &AdvancedCustomConfig{ Routes: []AdvancedCustomRoute{ { IncomingPath: tt.incomingPath, UpstreamPath: "/v1/chat/completions", Converter: advancedCustomConverterOpenAIResponsesToOpenAIChat, }, }, } err := config.Validate() require.Error(t, err) assert.Contains(t, err.Error(), "converter does not match incoming_path") }) } } func TestAdvancedCustomValidateDuplicateIncomingPathWithDisjointModels(t *testing.T) { config := &AdvancedCustomConfig{ Routes: []AdvancedCustomRoute{ { IncomingPath: "/v1/responses", UpstreamPath: "/v1/chat/completions", Converter: advancedCustomConverterOpenAIResponsesToOpenAIChat, Models: []string{"gpt-4o"}, }, { IncomingPath: "/v1/responses", UpstreamPath: "/v1beta/models/{model}:generateContent", Converter: advancedCustomConverterOpenAIResponsesToGemini, Models: []string{"gemini-2.5-flash"}, }, }, } require.NoError(t, config.Validate()) } func TestAdvancedCustomValidateDuplicateIncomingPathRejectsOverlappingModels(t *testing.T) { config := &AdvancedCustomConfig{ Routes: []AdvancedCustomRoute{ { IncomingPath: "/v1/responses", UpstreamPath: "/v1/chat/completions", Converter: advancedCustomConverterOpenAIResponsesToOpenAIChat, Models: []string{"shared-model"}, }, { IncomingPath: "/v1/responses", UpstreamPath: "/v1beta/models/{model}:generateContent", Converter: advancedCustomConverterOpenAIResponsesToGemini, Models: []string{"shared-model"}, }, }, } err := config.Validate() require.Error(t, err) assert.Contains(t, err.Error(), "models overlaps") } func TestAdvancedCustomValidateDuplicateIncomingPathRejectsMultipleCatchAllRoutes(t *testing.T) { config := &AdvancedCustomConfig{ Routes: []AdvancedCustomRoute{ { IncomingPath: "/v1/responses", UpstreamPath: "/v1/chat/completions", Converter: advancedCustomConverterOpenAIResponsesToOpenAIChat, }, { IncomingPath: "/v1/responses", UpstreamPath: "/v1beta/models/{model}:generateContent", Converter: advancedCustomConverterOpenAIResponsesToGemini, }, }, } err := config.Validate() require.Error(t, err) assert.Contains(t, err.Error(), "catch-all already exists") } func TestAdvancedCustomValidateDuplicateIncomingPathRequiresCatchAllLast(t *testing.T) { config := &AdvancedCustomConfig{ Routes: []AdvancedCustomRoute{ { IncomingPath: "/v1/responses", UpstreamPath: "/v1/chat/completions", Converter: advancedCustomConverterOpenAIResponsesToOpenAIChat, }, { IncomingPath: "/v1/responses", UpstreamPath: "/v1beta/models/{model}:generateContent", Converter: advancedCustomConverterOpenAIResponsesToGemini, Models: []string{"gemini-2.5-flash"}, }, }, } err := config.Validate() require.Error(t, err) assert.Contains(t, err.Error(), "catch-all route must be last") } func TestAdvancedCustomMatchPathForModel(t *testing.T) { config := &AdvancedCustomConfig{ Routes: []AdvancedCustomRoute{ { IncomingPath: "/v1/responses", UpstreamPath: "/v1beta/models/{model}:generateContent", Converter: advancedCustomConverterOpenAIResponsesToGemini, Models: []string{"gemini-2.5-flash"}, }, { IncomingPath: "/v1/responses", UpstreamPath: "/v1/chat/completions", Converter: advancedCustomConverterOpenAIResponsesToOpenAIChat, Models: []string{"gpt-4o"}, }, { IncomingPath: "/v1/responses", UpstreamPath: "/v1/responses", Converter: advancedCustomConverterNone, }, }, } require.NoError(t, config.Validate()) geminiRoute, ok := config.MatchPathForModel("/v1/responses", "gemini-2.5-flash") require.True(t, ok) assert.Equal(t, advancedCustomConverterOpenAIResponsesToGemini, geminiRoute.Converter) chatRoute, ok := config.MatchPathForModel("/v1/responses", "gpt-4o") require.True(t, ok) assert.Equal(t, advancedCustomConverterOpenAIResponsesToOpenAIChat, chatRoute.Converter) fallbackRoute, ok := config.MatchPathForModel("/v1/responses", "unknown-model") require.True(t, ok) assert.Equal(t, advancedCustomConverterNone, fallbackRoute.Converter) } func TestAdvancedCustomMatchPathForModelRegexRules(t *testing.T) { config := &AdvancedCustomConfig{ Routes: []AdvancedCustomRoute{ { IncomingPath: "/v1/responses", UpstreamPath: "/v1beta/models/{model}:generateContent", Converter: advancedCustomConverterOpenAIResponsesToGemini, Models: []string{"re:^gemini-"}, }, { IncomingPath: "/v1/responses", UpstreamPath: "/v1/chat/completions", Converter: advancedCustomConverterOpenAIResponsesToOpenAIChat, Models: []string{"re:(?i)^OAI-"}, }, { IncomingPath: "/v1/responses", UpstreamPath: "/v1/responses", Converter: advancedCustomConverterNone, }, }, } require.NoError(t, config.Validate()) geminiRoute, ok := config.MatchPathForModel("/v1/responses", "gemini-2.5-flash") require.True(t, ok) assert.Equal(t, advancedCustomConverterOpenAIResponsesToGemini, geminiRoute.Converter) chatRoute, ok := config.MatchPathForModel("/v1/responses", "oai-test") require.True(t, ok) assert.Equal(t, advancedCustomConverterOpenAIResponsesToOpenAIChat, chatRoute.Converter) fallbackRoute, ok := config.MatchPathForModel("/v1/responses", "gpt-4o") require.True(t, ok) assert.Equal(t, advancedCustomConverterNone, fallbackRoute.Converter) } func TestAdvancedCustomRouteModelRegexRulesAreCachedCompiled(t *testing.T) { require.True(t, matchAdvancedCustomRouteModelRule("re:^cache-probe-", "cache-probe-model")) cached, ok := advancedCustomModelRegexCache.Load("^cache-probe-") require.True(t, ok) require.NotNil(t, cached) _, isRegexp := cached.(*regexp.Regexp) require.True(t, isRegexp) // Invalid patterns never match and are cached as nil so they are not recompiled. require.False(t, matchAdvancedCustomRouteModelRule("re:(", "anything")) cached, ok = advancedCustomModelRegexCache.Load("(") require.True(t, ok) re, _ := cached.(*regexp.Regexp) require.Nil(t, re) // Cached entries keep matching correctly on subsequent calls. require.True(t, matchAdvancedCustomRouteModelRule("re:^cache-probe-", "cache-probe-other")) require.False(t, matchAdvancedCustomRouteModelRule("re:^cache-probe-", "other-model")) } func TestAdvancedCustomMatchPathForModelExactRuleDoesNotMatchPrefix(t *testing.T) { config := &AdvancedCustomConfig{ Routes: []AdvancedCustomRoute{ { IncomingPath: "/v1/responses", UpstreamPath: "/v1beta/models/{model}:generateContent", Converter: advancedCustomConverterOpenAIResponsesToGemini, Models: []string{"gemini"}, }, { IncomingPath: "/v1/responses", UpstreamPath: "/v1/responses", Converter: advancedCustomConverterNone, }, }, } require.NoError(t, config.Validate()) fallbackRoute, ok := config.MatchPathForModel("/v1/responses", "gemini-2.5-flash") require.True(t, ok) assert.Equal(t, advancedCustomConverterNone, fallbackRoute.Converter) } func TestAdvancedCustomValidateDuplicateIncomingPathRejectsInvalidRegexModels(t *testing.T) { tests := []struct { name string models []string want string }{ {name: "empty regex", models: []string{"re:"}, want: "regex is empty"}, {name: "invalid regex", models: []string{"re:["}, want: "regex is invalid"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { config := &AdvancedCustomConfig{ Routes: []AdvancedCustomRoute{ { IncomingPath: "/v1/responses", UpstreamPath: "/v1beta/models/{model}:generateContent", Converter: advancedCustomConverterOpenAIResponsesToGemini, Models: tt.models, }, }, } err := config.Validate() require.Error(t, err) assert.Contains(t, err.Error(), tt.want) }) } } func TestAdvancedCustomValidateDuplicateIncomingPathRejectsDuplicateRegexModels(t *testing.T) { config := &AdvancedCustomConfig{ Routes: []AdvancedCustomRoute{ { IncomingPath: "/v1/responses", UpstreamPath: "/v1beta/models/{model}:generateContent", Converter: advancedCustomConverterOpenAIResponsesToGemini, Models: []string{"re:^gemini-"}, }, { IncomingPath: "/v1/responses", UpstreamPath: "/v1/chat/completions", Converter: advancedCustomConverterOpenAIResponsesToOpenAIChat, Models: []string{"re:^gemini-"}, }, }, } err := config.Validate() require.Error(t, err) assert.Contains(t, err.Error(), "models overlaps") } func TestAdvancedCustomMatchPathForModelUsesFirstMatchingRegexRoute(t *testing.T) { config := &AdvancedCustomConfig{ Routes: []AdvancedCustomRoute{ { IncomingPath: "/v1/responses", UpstreamPath: "/v1beta/models/{model}:generateContent", Converter: advancedCustomConverterOpenAIResponsesToGemini, Models: []string{"re:^gemini-"}, }, { IncomingPath: "/v1/responses", UpstreamPath: "/v1/chat/completions", Converter: advancedCustomConverterOpenAIResponsesToOpenAIChat, Models: []string{"gemini-2.5-flash"}, }, }, } require.NoError(t, config.Validate()) route, ok := config.MatchPathForModel("/v1/responses", "gemini-2.5-flash") require.True(t, ok) assert.Equal(t, advancedCustomConverterOpenAIResponsesToGemini, route.Converter) } func TestAdvancedCustomSupportedEndpointTypesForModel(t *testing.T) { config := &AdvancedCustomConfig{ Routes: []AdvancedCustomRoute{ { IncomingPath: "/v1/responses", UpstreamPath: "/v1beta/models/{model}:generateContent", Converter: advancedCustomConverterOpenAIResponsesToGemini, Models: []string{"re:^gemini-"}, }, { IncomingPath: "/v1beta/models/{model}:generateContent", UpstreamPath: "/v1beta/models/{model}:generateContent", Models: []string{"re:^gemini-"}, }, { IncomingPath: "/v1beta/models/{model}:streamGenerateContent", UpstreamPath: "/v1beta/models/{model}:streamGenerateContent", Models: []string{"re:^gemini-"}, }, { IncomingPath: "/v1/chat/completions", UpstreamPath: "/v1/chat/completions", Models: []string{"gpt-4o"}, }, { IncomingPath: "/v1/messages", UpstreamPath: "/v1/messages", }, { IncomingPath: "/custom/endpoint", UpstreamPath: "/custom/endpoint", }, }, } require.NoError(t, config.Validate()) assert.Equal(t, []constant.EndpointType{ constant.EndpointTypeOpenAIResponse, constant.EndpointTypeGemini, constant.EndpointTypeAnthropic, }, config.SupportedEndpointTypesForModel("gemini-2.5-flash")) assert.Equal(t, []constant.EndpointType{ constant.EndpointTypeOpenAI, constant.EndpointTypeAnthropic, }, config.SupportedEndpointTypesForModel("gpt-4o")) assert.Equal(t, []constant.EndpointType{ constant.EndpointTypeAnthropic, }, config.SupportedEndpointTypesForModel("other-model")) }