package dto import ( "encoding/json" "regexp" "testing" "github.com/QuantumNous/new-api/relaykit/types" "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 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{ { 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, []types.EndpointType{ types.EndpointTypeOpenAIResponse, types.EndpointTypeGemini, types.EndpointTypeAnthropic, }, config.SupportedEndpointTypesForModel("gemini-2.5-flash")) assert.Equal(t, []types.EndpointType{ types.EndpointTypeOpenAI, types.EndpointTypeAnthropic, }, config.SupportedEndpointTypesForModel("gpt-4o")) assert.Equal(t, []types.EndpointType{ types.EndpointTypeAnthropic, }, config.SupportedEndpointTypesForModel("other-model")) } func TestAdvancedCustomValidateAlphaSearchConverterPath(t *testing.T) { valid := &AdvancedCustomConfig{ Routes: []AdvancedCustomRoute{ { IncomingPath: "/v1/alpha/search", UpstreamPath: "/v1/alpha/search", Converter: advancedCustomConverterNone, }, }, } require.NoError(t, valid.Validate()) assert.Equal(t, []types.EndpointType{ types.EndpointTypeOpenAIAlphaSearch, }, valid.SupportedEndpointTypesForModel("gpt-5.1")) nonNoneConverters := []string{ advancedCustomConverterClaudeMessagesToOpenAIChat, advancedCustomConverterOpenAIChatToClaudeMessages, advancedCustomConverterOpenAIChatToOpenAIResponses, advancedCustomConverterOpenAIResponsesToOpenAIChat, advancedCustomConverterOpenAIResponsesToGemini, advancedCustomConverterGeminiContentToOpenAIChat, advancedCustomConverterOpenAIChatToGeminiContent, } for _, converter := range nonNoneConverters { t.Run(converter, func(t *testing.T) { config := &AdvancedCustomConfig{ Routes: []AdvancedCustomRoute{ { IncomingPath: "/v1/alpha/search", UpstreamPath: "/v1/alpha/search", Converter: converter, }, }, } err := config.Validate() require.Error(t, err) assert.Contains(t, err.Error(), "converter does not match incoming_path") }) } } func TestChannelSettingsHTTPTransportJSONRoundTrip(t *testing.T) { legacy := `{"proxy":"http://127.0.0.1:8080","force_format":true}` var settings ChannelSettings require.NoError(t, json.Unmarshal([]byte(legacy), &settings)) assert.Equal(t, "http://127.0.0.1:8080", settings.Proxy) assert.True(t, settings.ForceFormat) assert.Empty(t, settings.HTTPProtocol) assert.Zero(t, settings.HTTP2ConnectionShards) encoded, err := json.Marshal(settings) require.NoError(t, err) assert.NotContains(t, string(encoded), "http_protocol") assert.NotContains(t, string(encoded), "http2_connection_shards") explicit := ChannelSettings{ Proxy: "socks5://127.0.0.1:1080", HTTPProtocol: HTTPProtocolHTTP1, HTTP2ConnectionShards: 1, } encoded, err = json.Marshal(explicit) require.NoError(t, err) assert.Contains(t, string(encoded), `"http_protocol":"http1"`) var decoded ChannelSettings require.NoError(t, json.Unmarshal(encoded, &decoded)) assert.Equal(t, explicit.HTTPProtocol, decoded.HTTPProtocol) assert.Equal(t, 1, decoded.HTTP2ConnectionShards) sharded := ChannelSettings{HTTP2ConnectionShards: 4} encoded, err = json.Marshal(sharded) require.NoError(t, err) assert.Contains(t, string(encoded), `"http2_connection_shards":4`) assert.NotContains(t, string(encoded), "http_protocol") } func TestChannelSettingsValidateHTTPTransport(t *testing.T) { require.NoError(t, (&ChannelSettings{}).ValidateHTTPTransport()) require.NoError(t, (&ChannelSettings{HTTPProtocol: "AUTO"}).ValidateHTTPTransport()) require.NoError(t, (&ChannelSettings{HTTPProtocol: "http1"}).ValidateHTTPTransport()) require.NoError(t, (&ChannelSettings{HTTP2ConnectionShards: 8}).ValidateHTTPTransport()) err := (&ChannelSettings{HTTPProtocol: "http2"}).ValidateHTTPTransport() require.Error(t, err) assert.Contains(t, err.Error(), "http_protocol") err = (&ChannelSettings{HTTP2ConnectionShards: -1}).ValidateHTTPTransport() require.Error(t, err) assert.Contains(t, err.Error(), "http2_connection_shards") err = (&ChannelSettings{HTTP2ConnectionShards: 9}).ValidateHTTPTransport() require.Error(t, err) assert.Contains(t, err.Error(), "http2_connection_shards") err = (&ChannelSettings{HTTPProtocol: "http1", HTTP2ConnectionShards: 2}).ValidateHTTPTransport() require.Error(t, err) assert.Contains(t, err.Error(), "http2_connection_shards") }