openai_images_actual_size_test.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. package service
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/base64"
  6. "encoding/binary"
  7. "fmt"
  8. "image"
  9. "image/color"
  10. "image/jpeg"
  11. "image/png"
  12. "io"
  13. "net/http"
  14. "net/http/httptest"
  15. "strings"
  16. "testing"
  17. "github.com/gin-gonic/gin"
  18. "github.com/stretchr/testify/require"
  19. "github.com/tidwall/gjson"
  20. )
  21. func TestDetectOpenAIImageResultSize(t *testing.T) {
  22. pngEncoded := encodeOpenAIImageTestPNG(t, 1672, 941)
  23. jpegEncoded := encodeOpenAIImageTestJPEG(t, 640, 360)
  24. webpVP8XEncoded := encodeOpenAIImageTestWebPVP8X(1920, 1080)
  25. webpVP8Encoded := encodeOpenAIImageTestWebPVP8(1280, 720)
  26. webpVP8LEncoded := encodeOpenAIImageTestWebPVP8L(640, 480)
  27. require.Equal(t, "1672x941", detectOpenAIImageResultSize(pngEncoded))
  28. require.Equal(t, "1672x941", detectOpenAIImageResultSize(strings.TrimRight(pngEncoded, "=")))
  29. require.Equal(t, "1672x941", detectOpenAIImageResultSize("data:image/png;base64,"+pngEncoded))
  30. require.Equal(t, "640x360", detectOpenAIImageResultSize(jpegEncoded))
  31. require.Equal(t, "1920x1080", detectOpenAIImageResultSize(webpVP8XEncoded))
  32. require.Equal(t, "1280x720", detectOpenAIImageResultSize(webpVP8Encoded))
  33. require.Equal(t, "640x480", detectOpenAIImageResultSize(webpVP8LEncoded))
  34. require.Empty(t, detectOpenAIImageResultSize("data:image/png;base64"))
  35. require.Empty(t, detectOpenAIImageResultSize("not-image-data"))
  36. }
  37. func TestOpenAIGatewayServiceForwardImages_OAuthUsesDecodedOutputDimensions(t *testing.T) {
  38. run := runOpenAIOAuthImageActualSizeTest(t, false)
  39. require.Equal(t, "3840x2160", gjson.GetBytes(run.upstream.lastBody, "tools.0.size").String())
  40. require.Equal(t, "low", gjson.GetBytes(run.upstream.lastBody, "tools.0.quality").String())
  41. require.Equal(t, "1672x941", gjson.Get(run.recorder.Body.String(), "size").String())
  42. require.Equal(t, "auto", gjson.Get(run.recorder.Body.String(), "quality").String())
  43. require.Equal(t, []string{"1672x941"}, run.result.ImageOutputSizes)
  44. ApplyOpenAIImageBillingResolution(run.result)
  45. require.Equal(t, ImageBillingSize2K, run.result.ImageSize)
  46. require.Equal(t, "1672x941", run.result.ImageOutputSize)
  47. require.Equal(t, ImageSizeSourceOutput, run.result.ImageSizeSource)
  48. }
  49. func TestOpenAIGatewayServiceForwardImages_OAuthStreamingUsesDecodedOutputDimensions(t *testing.T) {
  50. run := runOpenAIOAuthImageActualSizeTest(t, true)
  51. events := parseOpenAIImageTestSSEEvents(run.recorder.Body.String())
  52. completed, ok := findOpenAIImageTestSSEEvent(events, "image_generation.completed")
  53. require.True(t, ok)
  54. require.Equal(t, "1672x941", gjson.Get(completed.Data, "size").String())
  55. require.Equal(t, "auto", gjson.Get(completed.Data, "quality").String())
  56. require.Equal(t, []string{"1672x941"}, run.result.ImageOutputSizes)
  57. }
  58. type openAIOAuthImageActualSizeTestRun struct {
  59. result *OpenAIForwardResult
  60. recorder *httptest.ResponseRecorder
  61. upstream *httpUpstreamRecorder
  62. }
  63. func runOpenAIOAuthImageActualSizeTest(t *testing.T, stream bool) openAIOAuthImageActualSizeTestRun {
  64. t.Helper()
  65. gin.SetMode(gin.TestMode)
  66. body := []byte(fmt.Sprintf(`{"model":"gpt-image-2","prompt":"draw a test chart","size":"3840x2160","quality":"low","output_format":"png","stream":%t}`, stream))
  67. req := httptest.NewRequest(http.MethodPost, "/v1/images/generations", bytes.NewReader(body))
  68. req.Header.Set("Content-Type", "application/json")
  69. rec := httptest.NewRecorder()
  70. c, _ := gin.CreateTestContext(rec)
  71. c.Request = req
  72. c.Set("api_key", &APIKey{ID: 42})
  73. encoded := encodeOpenAIImageTestPNG(t, 1672, 941)
  74. upstreamBody := fmt.Sprintf(
  75. "data: {\"type\":\"response.created\",\"response\":{\"created_at\":1710000000,\"tools\":[{\"type\":\"image_generation\",\"model\":\"gpt-image-2\",\"size\":\"auto\",\"quality\":\"auto\",\"output_format\":\"png\"}]}}\n\n"+
  76. "data: {\"type\":\"response.completed\",\"response\":{\"created_at\":1710000000,\"tools\":[{\"type\":\"image_generation\",\"model\":\"gpt-image-2\",\"size\":\"auto\",\"quality\":\"auto\",\"output_format\":\"png\"}],\"output\":[{\"id\":\"ig_actual_size\",\"type\":\"image_generation_call\",\"result\":%q}]}}\n\n"+
  77. "data: [DONE]\n\n",
  78. encoded,
  79. )
  80. upstream := &httpUpstreamRecorder{resp: &http.Response{
  81. StatusCode: http.StatusOK,
  82. Header: http.Header{
  83. "Content-Type": []string{"text/event-stream"},
  84. "X-Request-Id": []string{"req_img_actual_size"},
  85. },
  86. Body: io.NopCloser(strings.NewReader(upstreamBody)),
  87. }}
  88. svc := &OpenAIGatewayService{httpUpstream: upstream}
  89. parsed, err := svc.ParseOpenAIImagesRequest(c, body)
  90. require.NoError(t, err)
  91. account := &Account{
  92. ID: 1,
  93. Name: "openai-oauth",
  94. Platform: PlatformOpenAI,
  95. Type: AccountTypeOAuth,
  96. Credentials: map[string]any{
  97. "access_token": "token-123",
  98. "chatgpt_account_id": "acct-123",
  99. },
  100. }
  101. result, err := svc.ForwardImages(context.Background(), c, account, body, parsed, "")
  102. require.NoError(t, err)
  103. require.NotNil(t, result)
  104. return openAIOAuthImageActualSizeTestRun{result: result, recorder: rec, upstream: upstream}
  105. }
  106. func encodeOpenAIImageTestPNG(t *testing.T, width, height int) string {
  107. t.Helper()
  108. img := image.NewNRGBA(image.Rect(0, 0, width, height))
  109. img.SetNRGBA(0, 0, color.NRGBA{R: 0xff, A: 0xff})
  110. var buf bytes.Buffer
  111. require.NoError(t, png.Encode(&buf, img))
  112. return base64.StdEncoding.EncodeToString(buf.Bytes())
  113. }
  114. func encodeOpenAIImageTestJPEG(t *testing.T, width, height int) string {
  115. t.Helper()
  116. img := image.NewNRGBA(image.Rect(0, 0, width, height))
  117. img.SetNRGBA(0, 0, color.NRGBA{G: 0xff, A: 0xff})
  118. var buf bytes.Buffer
  119. require.NoError(t, jpeg.Encode(&buf, img, nil))
  120. return base64.StdEncoding.EncodeToString(buf.Bytes())
  121. }
  122. func encodeOpenAIImageTestWebPVP8X(width, height int) string {
  123. header := make([]byte, 30)
  124. copy(header[0:4], "RIFF")
  125. copy(header[8:12], "WEBP")
  126. copy(header[12:16], "VP8X")
  127. width--
  128. height--
  129. header[24], header[25], header[26] = byte(width), byte(width>>8), byte(width>>16)
  130. header[27], header[28], header[29] = byte(height), byte(height>>8), byte(height>>16)
  131. return base64.StdEncoding.EncodeToString(header)
  132. }
  133. func encodeOpenAIImageTestWebPVP8(width, height int) string {
  134. header := make([]byte, 30)
  135. copy(header[0:4], "RIFF")
  136. copy(header[8:12], "WEBP")
  137. copy(header[12:16], "VP8 ")
  138. copy(header[23:26], "\x9d\x01\x2a")
  139. binary.LittleEndian.PutUint16(header[26:28], uint16(width))
  140. binary.LittleEndian.PutUint16(header[28:30], uint16(height))
  141. return base64.StdEncoding.EncodeToString(header)
  142. }
  143. func encodeOpenAIImageTestWebPVP8L(width, height int) string {
  144. header := make([]byte, 25)
  145. copy(header[0:4], "RIFF")
  146. copy(header[8:12], "WEBP")
  147. copy(header[12:16], "VP8L")
  148. header[20] = 0x2f
  149. width--
  150. height--
  151. header[21] = byte(width)
  152. header[22] = byte(width>>8)&0x3f | byte(height&0x03)<<6
  153. header[23] = byte(height >> 2)
  154. header[24] = byte(height>>10) & 0x0f
  155. return base64.StdEncoding.EncodeToString(header)
  156. }