package handler import ( "net/http/httptest" "testing" middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" ) func TestCompositeTargetPlatformAllowedResolvesKnownAllowedModel(t *testing.T) { gin.SetMode(gin.TestMode) c, _ := gin.CreateTestContext(httptest.NewRecorder()) c.Request = httptest.NewRequest("POST", "/v1/embeddings", nil) apiKey := &service.APIKey{Group: &service.Group{Platform: service.PlatformComposite}} require.True(t, compositeTargetPlatformAllowed(c, apiKey, "text-embedding-3-large", service.PlatformOpenAI)) platform, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context()) require.True(t, ok) require.Equal(t, service.PlatformOpenAI, platform) } func TestOpenAICompatibleTextTargetAllowsCompositeProviders(t *testing.T) { gin.SetMode(gin.TestMode) providers := []struct { model string platform string }{ {model: "grok-4.3", platform: service.PlatformGrok}, {model: "kimi-k2-thinking", platform: service.PlatformKimi}, {model: "glm-5.2", platform: service.PlatformZhipu}, {model: "deepseek-v3.2", platform: service.PlatformDeepseek}, } for _, path := range []string{"/v1/messages", "/v1/chat/completions", "/v1/responses", "/v1/responses/input_tokens", "/v1/messages/count_tokens"} { for _, provider := range providers { c, _ := gin.CreateTestContext(httptest.NewRecorder()) c.Request = httptest.NewRequest("POST", path, nil) apiKey := &service.APIKey{Group: &service.Group{Platform: service.PlatformComposite}} require.True(t, openAICompatibleTextTargetAllowed(c, apiKey, provider.model), "path=%s model=%s", path, provider.model) platform, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context()) require.True(t, ok, "path=%s model=%s", path, provider.model) require.Equal(t, provider.platform, platform, "path=%s model=%s", path, provider.model) } } } // WS ingress 对 CN 账号既过不了 transport 过滤、HTTP 桥也没有 Responses 转换, // 放行只会把明确的策略拒绝换成 "no available account",因此 WS 白名单保持 openai+grok。 func TestResponsesWebSocketCompositePlatformGuardKeepsOpenAIAndGrokOnly(t *testing.T) { require.True(t, isResponsesWebSocketCompositePlatform(service.PlatformOpenAI)) require.True(t, isResponsesWebSocketCompositePlatform(service.PlatformGrok)) for _, platform := range []string{ service.PlatformKimi, service.PlatformZhipu, service.PlatformDeepseek, service.PlatformAnthropic, service.PlatformGemini, } { require.False(t, isResponsesWebSocketCompositePlatform(platform), "platform=%s", platform) } } func TestCompositeTargetPlatformAllowedRejectsWrongOrUnknownModel(t *testing.T) { gin.SetMode(gin.TestMode) for _, tc := range []struct { name string model string }{ {name: "wrong provider", model: "claude-sonnet-4-5"}, {name: "unknown provider", model: "llama-4-maverick"}, } { t.Run(tc.name, func(t *testing.T) { c, _ := gin.CreateTestContext(httptest.NewRecorder()) c.Request = httptest.NewRequest("POST", "/v1/embeddings", nil) apiKey := &service.APIKey{Group: &service.Group{Platform: service.PlatformComposite}} require.False(t, compositeTargetPlatformAllowed(c, apiKey, tc.model, service.PlatformOpenAI)) }) } } func TestCompositeTargetPlatformResolvedRejectsUnknownModel(t *testing.T) { gin.SetMode(gin.TestMode) c, _ := gin.CreateTestContext(httptest.NewRecorder()) c.Request = httptest.NewRequest("POST", "/v1/messages", nil) apiKey := &service.APIKey{Group: &service.Group{Platform: service.PlatformComposite}} require.False(t, compositeTargetPlatformResolved(c, apiKey, "llama-4-maverick")) _, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context()) require.False(t, ok) } func TestCompositeTargetPlatformResolvedAllowsConcreteGroupWithoutResolution(t *testing.T) { gin.SetMode(gin.TestMode) c, _ := gin.CreateTestContext(httptest.NewRecorder()) c.Request = httptest.NewRequest("POST", "/v1/messages", nil) apiKey := &service.APIKey{Group: &service.Group{Platform: service.PlatformAnthropic}} require.True(t, compositeTargetPlatformResolved(c, apiKey, "llama-4-maverick")) } func TestOpenAIReasoningEffortPolicyForCompositeTarget(t *testing.T) { gin.SetMode(gin.TestMode) group := &service.Group{ Platform: service.PlatformComposite, MaxReasoningEffort: "medium", ReasoningEffortMappings: []service.ReasoningEffortMapping{ {From: "max", To: "xhigh"}, }, } apiKey := &service.APIKey{Group: group} body := []byte(`{"reasoning":{"effort":"max"}}`) openAICtx, _ := gin.CreateTestContext(httptest.NewRecorder()) openAICtx.Request = httptest.NewRequest("POST", "/v1/responses", nil) openAICtx.Request = openAICtx.Request.WithContext(service.WithResolvedTargetPlatform(openAICtx.Request.Context(), service.PlatformOpenAI)) got, changed := applyOpenAIReasoningEffortPolicyForRequest(openAICtx, apiKey, body) require.True(t, changed) require.JSONEq(t, `{"reasoning":{"effort":"medium"}}`, string(got)) bindOpenAIReasoningEffortPolicyForMessagesRequest(openAICtx, apiKey, []byte(`{"output_config":{"effort":"max"}}`)) bound, changed := service.ApplyOpenAIReasoningEffortPolicyFromContext(openAICtx.Request.Context(), body) require.True(t, changed) require.JSONEq(t, `{"reasoning":{"effort":"medium"}}`, string(bound)) omittedCtx, _ := gin.CreateTestContext(httptest.NewRecorder()) omittedCtx.Request = httptest.NewRequest("POST", "/v1/messages", nil) omittedCtx.Request = omittedCtx.Request.WithContext(service.WithResolvedTargetPlatform(omittedCtx.Request.Context(), service.PlatformOpenAI)) bindOpenAIReasoningEffortPolicyForMessagesRequest(omittedCtx, apiKey, []byte(`{"model":"gpt-5"}`)) omitted, changed := service.ApplyOpenAIReasoningEffortPolicyFromContext(omittedCtx.Request.Context(), body) require.False(t, changed) require.Equal(t, body, omitted) grokCtx, _ := gin.CreateTestContext(httptest.NewRecorder()) grokCtx.Request = httptest.NewRequest("POST", "/v1/responses", nil) grokCtx.Request = grokCtx.Request.WithContext(service.WithResolvedTargetPlatform(grokCtx.Request.Context(), service.PlatformGrok)) got, changed = applyOpenAIReasoningEffortPolicyForRequest(grokCtx, apiKey, body) require.False(t, changed) require.Equal(t, body, got) } func TestClientRequestedModelUsesCompositePublicModel(t *testing.T) { gin.SetMode(gin.TestMode) c, _ := gin.CreateTestContext(httptest.NewRecorder()) c.Request = httptest.NewRequest("POST", "/v1/chat/completions", nil) c.Request = c.Request.WithContext(service.WithCompositeRouteDecision(c.Request.Context(), service.CompositeRouteDecision{ Matched: true, Source: service.CompositeRouteSourceExplicit, PublicModel: "public-alias", TargetPlatform: service.PlatformOpenAI, UpstreamModel: "gpt-5", })) input := buildContentModerationInput(c, nil, middleware2.AuthSubject{UserID: 42}, service.ContentModerationProtocolOpenAIChat, "gpt-5", nil) require.Equal(t, "public-alias", input.Model) require.Equal(t, service.PlatformOpenAI, input.Provider) fields := clientRequestedUsageFields(c, service.ChannelMappingResult{MappedModel: "gpt-5"}, "gpt-5", "gpt-5") require.Equal(t, "public-alias", fields.OriginalModel) require.Equal(t, "public-alias", fields.ChannelMappedModel) require.Equal(t, "public-alias\u2192gpt-5", fields.ModelMappingChain) }