grok_media_test.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. package handler
  2. import (
  3. "context"
  4. "errors"
  5. "strings"
  6. "testing"
  7. "github.com/Wei-Shaw/sub2api/internal/service"
  8. "github.com/stretchr/testify/require"
  9. )
  10. type grokMediaEligibilityProberStub struct {
  11. eligible bool
  12. reason string
  13. err error
  14. calls int
  15. }
  16. func (s *grokMediaEligibilityProberStub) ProbeMediaEligibility(context.Context, int64) (bool, string, error) {
  17. s.calls++
  18. return s.eligible, s.reason, s.err
  19. }
  20. func TestShouldRecordGrokMediaUsage(t *testing.T) {
  21. tests := []struct {
  22. name string
  23. endpoint service.GrokMediaEndpoint
  24. model string
  25. want bool
  26. }{
  27. {
  28. name: "image generation records usage",
  29. endpoint: service.GrokMediaEndpointImagesGenerations,
  30. model: "grok-imagine",
  31. want: true,
  32. },
  33. {
  34. name: "image edit records usage",
  35. endpoint: service.GrokMediaEndpointImagesEdits,
  36. model: "grok-imagine-edit",
  37. want: true,
  38. },
  39. {
  40. name: "video generation defers usage until status",
  41. endpoint: service.GrokMediaEndpointVideosGenerations,
  42. model: "grok-imagine-video-1.5",
  43. want: false,
  44. },
  45. {
  46. name: "video status skips immediate helper (status path claims separately)",
  47. endpoint: service.GrokMediaEndpointVideoStatus,
  48. model: "",
  49. want: false,
  50. },
  51. {
  52. name: "video content skips usage",
  53. endpoint: service.GrokMediaEndpointVideoContent,
  54. model: "",
  55. want: false,
  56. },
  57. {
  58. name: "generation skips usage without model",
  59. endpoint: service.GrokMediaEndpointImagesGenerations,
  60. model: " ",
  61. want: false,
  62. },
  63. }
  64. for _, tt := range tests {
  65. t.Run(tt.name, func(t *testing.T) {
  66. // Nil result must never bill.
  67. require.False(t, shouldRecordGrokMediaUsage(tt.endpoint, tt.model, nil))
  68. // Immediate helper only bills image generation (async video bills on status).
  69. result := &service.OpenAIForwardResult{ImageCount: 1, VideoCount: 0}
  70. if tt.endpoint.IsGenerationRequest() && !isGrokVideoCreateEndpoint(tt.endpoint) && strings.TrimSpace(tt.model) != "" {
  71. require.Equal(t, tt.want, shouldRecordGrokMediaUsage(tt.endpoint, tt.model, result))
  72. } else {
  73. require.False(t, shouldRecordGrokMediaUsage(tt.endpoint, tt.model, result))
  74. }
  75. // Zero billable units never bill even for generation + model.
  76. empty := &service.OpenAIForwardResult{}
  77. require.False(t, shouldRecordGrokMediaUsage(tt.endpoint, tt.model, empty))
  78. })
  79. }
  80. }
  81. func TestGrokMediaRequiredCapability(t *testing.T) {
  82. tests := []struct {
  83. name string
  84. endpoint service.GrokMediaEndpoint
  85. want service.OpenAIEndpointCapability
  86. }{
  87. {name: "image generation", endpoint: service.GrokMediaEndpointImagesGenerations, want: service.OpenAIEndpointCapabilityGrokMediaGeneration},
  88. {name: "image edit", endpoint: service.GrokMediaEndpointImagesEdits, want: service.OpenAIEndpointCapabilityGrokMediaGeneration},
  89. {name: "video generation", endpoint: service.GrokMediaEndpointVideosGenerations, want: service.OpenAIEndpointCapabilityGrokMediaGeneration},
  90. {name: "video edit", endpoint: service.GrokMediaEndpointVideosEdits, want: service.OpenAIEndpointCapabilityGrokMediaGeneration},
  91. {name: "video extension", endpoint: service.GrokMediaEndpointVideosExtensions, want: service.OpenAIEndpointCapabilityGrokMediaGeneration},
  92. {name: "video status preserves lookup", endpoint: service.GrokMediaEndpointVideoStatus, want: ""},
  93. {name: "video content preserves lookup", endpoint: service.GrokMediaEndpointVideoContent, want: ""},
  94. }
  95. for _, tt := range tests {
  96. t.Run(tt.name, func(t *testing.T) {
  97. require.Equal(t, tt.want, grokMediaRequiredCapability(tt.endpoint))
  98. })
  99. }
  100. }
  101. func TestGrokMediaScheduleModelUsesNormalizedMappedUpstream(t *testing.T) {
  102. account := &service.Account{
  103. Platform: service.PlatformGrok,
  104. Credentials: map[string]any{
  105. "model_mapping": map[string]any{
  106. "grok-imagine-video-1.5": "wrong-raw-model",
  107. "grok-imagine-video": "mapped-video-model",
  108. },
  109. },
  110. }
  111. require.Equal(t, "mapped-video-model", grokMediaScheduleModel(account, "grok-imagine-video", nil))
  112. require.Equal(t, "actual-upstream-model", grokMediaScheduleModel(account, "grok-imagine-video", &service.OpenAIForwardResult{
  113. UpstreamModel: "actual-upstream-model",
  114. }))
  115. require.Equal(t, "mapped-video-model", grokMediaScheduleModel(account, "grok-imagine-video", &service.OpenAIForwardResult{}))
  116. require.Equal(t, "grok-imagine-video", grokMediaScheduleModel(nil, " grok-imagine-video ", nil))
  117. }
  118. func TestEnsureGrokMediaAccountEligibility(t *testing.T) {
  119. t.Run("non oauth account does not probe", func(t *testing.T) {
  120. prober := &grokMediaEligibilityProberStub{}
  121. h := &OpenAIGatewayHandler{grokMediaEligibilityProber: prober}
  122. account := &service.Account{Platform: service.PlatformGrok, Type: service.AccountTypeAPIKey}
  123. eligible, reason, err := h.ensureGrokMediaAccountEligibility(context.Background(), account)
  124. require.NoError(t, err)
  125. require.True(t, eligible)
  126. require.Equal(t, "non_oauth", reason)
  127. require.Zero(t, prober.calls)
  128. })
  129. t.Run("unobserved oauth is probed before forwarding", func(t *testing.T) {
  130. prober := &grokMediaEligibilityProberStub{eligible: true, reason: "eligible"}
  131. h := &OpenAIGatewayHandler{grokMediaEligibilityProber: prober}
  132. account := &service.Account{ID: 7, Platform: service.PlatformGrok, Type: service.AccountTypeOAuth}
  133. eligible, reason, err := h.ensureGrokMediaAccountEligibility(context.Background(), account)
  134. require.NoError(t, err)
  135. require.True(t, eligible)
  136. require.Equal(t, "eligible", reason)
  137. require.Equal(t, 1, prober.calls)
  138. })
  139. t.Run("missing prober fails closed", func(t *testing.T) {
  140. h := &OpenAIGatewayHandler{}
  141. account := &service.Account{ID: 8, Platform: service.PlatformGrok, Type: service.AccountTypeOAuth}
  142. eligible, reason, err := h.ensureGrokMediaAccountEligibility(context.Background(), account)
  143. require.Error(t, err)
  144. require.False(t, eligible)
  145. require.Equal(t, "billing_probe_unavailable", reason)
  146. })
  147. t.Run("probe failure fails closed", func(t *testing.T) {
  148. probeErr := errors.New("probe failed")
  149. prober := &grokMediaEligibilityProberStub{reason: "billing_unobserved", err: probeErr}
  150. h := &OpenAIGatewayHandler{grokMediaEligibilityProber: prober}
  151. account := &service.Account{ID: 9, Platform: service.PlatformGrok, Type: service.AccountTypeOAuth}
  152. eligible, reason, err := h.ensureGrokMediaAccountEligibility(context.Background(), account)
  153. require.ErrorIs(t, err, probeErr)
  154. require.False(t, eligible)
  155. require.Equal(t, "billing_unobserved", reason)
  156. })
  157. }