2026-04-22 12:30:08 +08:00
package service
import (
"bytes"
2026-04-23 09:53:57 +08:00
"context"
2026-04-23 12:44:44 +08:00
"io"
2026-04-22 12:30:08 +08:00
"mime/multipart"
"net/http"
"net/http/httptest"
2026-04-23 12:44:44 +08:00
"net/textproto"
"strings"
2026-04-22 12:30:08 +08:00
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
2026-04-23 12:44:44 +08:00
"github.com/tidwall/gjson"
2026-04-22 12:30:08 +08:00
)
func TestOpenAIGatewayServiceParseOpenAIImagesRequest_JSON ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
body := [] byte ( `{"model":"gpt-image-2","prompt":"draw a cat","size":"1024x1024","quality":"high","stream":true}` )
req := httptest . NewRequest ( http . MethodPost , "/v1/images/generations" , bytes . NewReader ( body ))
req . Header . Set ( "Content-Type" , "application/json" )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
c . Request = req
svc := & OpenAIGatewayService {}
parsed , err := svc . ParseOpenAIImagesRequest ( c , body )
require . NoError ( t , err )
require . NotNil ( t , parsed )
require . Equal ( t , "/v1/images/generations" , parsed . Endpoint )
require . Equal ( t , "gpt-image-2" , parsed . Model )
require . Equal ( t , "draw a cat" , parsed . Prompt )
require . True ( t , parsed . Stream )
require . Equal ( t , "1024x1024" , parsed . Size )
require . Equal ( t , "1K" , parsed . SizeTier )
require . Equal ( t , OpenAIImagesCapabilityNative , parsed . RequiredCapability )
require . False ( t , parsed . Multipart )
}
func TestOpenAIGatewayServiceParseOpenAIImagesRequest_MultipartEdit ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
var body bytes . Buffer
writer := multipart . NewWriter ( & body )
require . NoError ( t , writer . WriteField ( "model" , "gpt-image-2" ))
require . NoError ( t , writer . WriteField ( "prompt" , "replace background" ))
require . NoError ( t , writer . WriteField ( "size" , "1536x1024" ))
part , err := writer . CreateFormFile ( "image" , "source.png" )
require . NoError ( t , err )
_ , err = part . Write ([] byte ( "fake-image-bytes" ))
require . NoError ( t , err )
require . NoError ( t , writer . Close ())
req := httptest . NewRequest ( http . MethodPost , "/v1/images/edits" , bytes . NewReader ( body . Bytes ()))
req . Header . Set ( "Content-Type" , writer . FormDataContentType ())
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
c . Request = req
svc := & OpenAIGatewayService {}
parsed , err := svc . ParseOpenAIImagesRequest ( c , body . Bytes ())
require . NoError ( t , err )
require . NotNil ( t , parsed )
require . Equal ( t , "/v1/images/edits" , parsed . Endpoint )
require . True ( t , parsed . Multipart )
require . Equal ( t , "gpt-image-2" , parsed . Model )
require . Equal ( t , "replace background" , parsed . Prompt )
require . Equal ( t , "1536x1024" , parsed . Size )
require . Equal ( t , "2K" , parsed . SizeTier )
require . Len ( t , parsed . Uploads , 1 )
require . Equal ( t , OpenAIImagesCapabilityNative , parsed . RequiredCapability )
}
2026-04-23 12:44:44 +08:00
func TestOpenAIGatewayServiceParseOpenAIImagesRequest_MultipartEditWithMaskAndNativeOptions ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
var body bytes . Buffer
writer := multipart . NewWriter ( & body )
require . NoError ( t , writer . WriteField ( "model" , "gpt-image-2" ))
require . NoError ( t , writer . WriteField ( "prompt" , "replace foreground" ))
require . NoError ( t , writer . WriteField ( "output_format" , "png" ))
require . NoError ( t , writer . WriteField ( "input_fidelity" , "high" ))
require . NoError ( t , writer . WriteField ( "output_compression" , "80" ))
require . NoError ( t , writer . WriteField ( "partial_images" , "2" ))
imageHeader := make ( textproto . MIMEHeader )
imageHeader . Set ( "Content-Disposition" , `form-data; name="image"; filename="source.png"` )
imageHeader . Set ( "Content-Type" , "image/png" )
imagePart , err := writer . CreatePart ( imageHeader )
require . NoError ( t , err )
_ , err = imagePart . Write ([] byte ( "source-image-bytes" ))
require . NoError ( t , err )
maskHeader := make ( textproto . MIMEHeader )
maskHeader . Set ( "Content-Disposition" , `form-data; name="mask"; filename="mask.png"` )
maskHeader . Set ( "Content-Type" , "image/png" )
maskPart , err := writer . CreatePart ( maskHeader )
require . NoError ( t , err )
_ , err = maskPart . Write ([] byte ( "mask-image-bytes" ))
require . NoError ( t , err )
require . NoError ( t , writer . Close ())
req := httptest . NewRequest ( http . MethodPost , "/v1/images/edits" , bytes . NewReader ( body . Bytes ()))
req . Header . Set ( "Content-Type" , writer . FormDataContentType ())
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
c . Request = req
svc := & OpenAIGatewayService {}
parsed , err := svc . ParseOpenAIImagesRequest ( c , body . Bytes ())
require . NoError ( t , err )
require . NotNil ( t , parsed )
require . Len ( t , parsed . Uploads , 1 )
require . NotNil ( t , parsed . MaskUpload )
require . True ( t , parsed . HasMask )
require . Equal ( t , "png" , parsed . OutputFormat )
require . Equal ( t , "high" , parsed . InputFidelity )
require . NotNil ( t , parsed . OutputCompression )
require . Equal ( t , 80 , * parsed . OutputCompression )
require . NotNil ( t , parsed . PartialImages )
require . Equal ( t , 2 , * parsed . PartialImages )
require . Equal ( t , OpenAIImagesCapabilityNative , parsed . RequiredCapability )
}
2026-04-22 12:30:08 +08:00
func TestOpenAIGatewayServiceParseOpenAIImagesRequest_PromptOnlyDefaultsRemainBasic ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
body := [] byte ( `{"prompt":"draw a cat"}` )
req := httptest . NewRequest ( http . MethodPost , "/v1/images/generations" , bytes . NewReader ( body ))
req . Header . Set ( "Content-Type" , "application/json" )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
c . Request = req
svc := & OpenAIGatewayService {}
parsed , err := svc . ParseOpenAIImagesRequest ( c , body )
require . NoError ( t , err )
require . NotNil ( t , parsed )
require . Equal ( t , "gpt-image-2" , parsed . Model )
require . Equal ( t , OpenAIImagesCapabilityBasic , parsed . RequiredCapability )
}
func TestOpenAIGatewayServiceParseOpenAIImagesRequest_ExplicitSizeRequiresNativeCapability ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
body := [] byte ( `{"prompt":"draw a cat","size":"1024x1024"}` )
req := httptest . NewRequest ( http . MethodPost , "/v1/images/generations" , bytes . NewReader ( body ))
req . Header . Set ( "Content-Type" , "application/json" )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
c . Request = req
svc := & OpenAIGatewayService {}
parsed , err := svc . ParseOpenAIImagesRequest ( c , body )
require . NoError ( t , err )
require . NotNil ( t , parsed )
require . Equal ( t , OpenAIImagesCapabilityNative , parsed . RequiredCapability )
}
2026-04-23 09:53:57 +08:00
func TestOpenAIGatewayServiceParseOpenAIImagesRequest_RejectsNonImageModel ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
body := [] byte ( `{"model":"gpt-5.4","prompt":"draw a cat"}` )
req := httptest . NewRequest ( http . MethodPost , "/v1/images/generations" , bytes . NewReader ( body ))
req . Header . Set ( "Content-Type" , "application/json" )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
c . Request = req
svc := & OpenAIGatewayService {}
parsed , err := svc . ParseOpenAIImagesRequest ( c , body )
require . Nil ( t , parsed )
require . ErrorContains ( t , err , `images endpoint requires an image model, got "gpt-5.4"` )
}
2026-04-23 12:44:44 +08:00
func TestOpenAIGatewayServiceParseOpenAIImagesRequest_JSONEditURLs ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
body := [] byte ( `{
"model":"gpt-image-2",
"prompt":"replace the background",
"images":[{"image_url":"https://example.com/source.png"}],
"mask":{"image_url":"https://example.com/mask.png"},
"input_fidelity":"high",
"output_compression":90,
"partial_images":2,
"response_format":"url"
}` )
req := httptest . NewRequest ( http . MethodPost , "/v1/images/edits" , bytes . NewReader ( body ))
req . Header . Set ( "Content-Type" , "application/json" )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
c . Request = req
svc := & OpenAIGatewayService {}
parsed , err := svc . ParseOpenAIImagesRequest ( c , body )
require . NoError ( t , err )
require . NotNil ( t , parsed )
require . Equal ( t , [] string { "https://example.com/source.png" }, parsed . InputImageURLs )
require . Equal ( t , "https://example.com/mask.png" , parsed . MaskImageURL )
require . Equal ( t , "high" , parsed . InputFidelity )
require . NotNil ( t , parsed . OutputCompression )
require . Equal ( t , 90 , * parsed . OutputCompression )
require . NotNil ( t , parsed . PartialImages )
require . Equal ( t , 2 , * parsed . PartialImages )
require . True ( t , parsed . HasMask )
require . Equal ( t , OpenAIImagesCapabilityNative , parsed . RequiredCapability )
}
2026-04-23 09:53:57 +08:00
func TestCollectOpenAIImagePointers_RecognizesDirectAssets ( t * testing . T ) {
items := collectOpenAIImagePointers ([] byte ( `{
"revised_prompt": "cat astronaut",
"parts": [
{"b64_json":"QUJD"},
{"download_url":"https://files.example.com/image.png?sig=1"},
{"asset_pointer":"file-service://file_123"}
]
}` ))
require . Len ( t , items , 3 )
var sawBase64 , sawURL , sawPointer bool
for _ , item := range items {
if item . B64JSON == "QUJD" {
sawBase64 = true
require . Equal ( t , "cat astronaut" , item . Prompt )
}
if item . DownloadURL == "https://files.example.com/image.png?sig=1" {
sawURL = true
}
if item . Pointer == "file-service://file_123" {
sawPointer = true
}
}
require . True ( t , sawBase64 )
require . True ( t , sawURL )
require . True ( t , sawPointer )
}
func TestResolveOpenAIImageBytes_PrefersInlineBase64 ( t * testing . T ) {
data , err := resolveOpenAIImageBytes ( context . Background (), nil , nil , "" , openAIImagePointerInfo {
B64JSON : "data:image/png;base64,QUJD" ,
})
require . NoError ( t , err )
require . Equal ( t , [] byte ( "ABC" ), data )
}
2026-04-23 12:44:44 +08:00
func TestAccountSupportsOpenAIImageCapability_OAuthSupportsNative ( t * testing . T ) {
account := & Account {
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
}
require . True ( t , account . SupportsOpenAIImageCapability ( OpenAIImagesCapabilityBasic ))
require . True ( t , account . SupportsOpenAIImageCapability ( OpenAIImagesCapabilityNative ))
}
func TestOpenAIGatewayServiceForwardImages_OAuthUsesResponsesAPI ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
body := [] byte ( `{"model":"gpt-image-2","prompt":"draw a cat","size":"1024x1024","quality":"high"}` )
req := httptest . NewRequest ( http . MethodPost , "/v1/images/generations" , bytes . NewReader ( body ))
req . Header . Set ( "Content-Type" , "application/json" )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
c . Request = req
c . Set ( "api_key" , & APIKey { ID : 42 })
svc := & OpenAIGatewayService {}
parsed , err := svc . ParseOpenAIImagesRequest ( c , body )
require . NoError ( t , err )
upstream := & httpUpstreamRecorder {
resp : & http . Response {
StatusCode : http . StatusOK ,
Header : http . Header {
"Content-Type" : [] string { "text/event-stream" },
"X-Request-Id" : [] string { "req_img_123" },
},
Body : io . NopCloser ( strings . NewReader (
"data: {\"type\":\"response.completed\",\"response\":{\"created_at\":1710000000,\"usage\":{\"input_tokens\":11,\"output_tokens\":22,\"input_tokens_details\":{\"cached_tokens\":3},\"output_tokens_details\":{\"image_tokens\":7}},\"tool_usage\":{\"image_gen\":{\"images\":1}},\"output\":[{\"type\":\"image_generation_call\",\"result\":\"aGVsbG8=\",\"revised_prompt\":\"draw a cat\",\"output_format\":\"png\",\"quality\":\"high\",\"size\":\"1024x1024\"}]}}\n\n" +
"data: [DONE]\n\n" ,
)),
},
}
svc . httpUpstream = upstream
account := & Account {
ID : 1 ,
Name : "openai-oauth" ,
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
Credentials : map [ string ] any {
"access_token" : "token-123" ,
"chatgpt_account_id" : "acct-123" ,
},
}
result , err := svc . ForwardImages ( context . Background (), c , account , body , parsed , "" )
require . NoError ( t , err )
require . NotNil ( t , result )
require . Equal ( t , "gpt-image-2" , result . Model )
require . Equal ( t , "gpt-image-2" , result . UpstreamModel )
require . Equal ( t , 1 , result . ImageCount )
require . Equal ( t , 11 , result . Usage . InputTokens )
require . Equal ( t , 22 , result . Usage . OutputTokens )
require . Equal ( t , 7 , result . Usage . ImageOutputTokens )
require . NotNil ( t , upstream . lastReq )
require . Equal ( t , chatgptCodexURL , upstream . lastReq . URL . String ())
require . Equal ( t , "chatgpt.com" , upstream . lastReq . Host )
require . Equal ( t , "application/json" , upstream . lastReq . Header . Get ( "Content-Type" ))
require . Equal ( t , "text/event-stream" , upstream . lastReq . Header . Get ( "Accept" ))
require . Equal ( t , "acct-123" , upstream . lastReq . Header . Get ( "chatgpt-account-id" ))
require . Equal ( t , "responses=experimental" , upstream . lastReq . Header . Get ( "OpenAI-Beta" ))
require . Equal ( t , openAIImagesResponsesMainModel , gjson . GetBytes ( upstream . lastBody , "model" ). String ())
require . True ( t , gjson . GetBytes ( upstream . lastBody , "stream" ). Bool ())
require . Equal ( t , "image_generation" , gjson . GetBytes ( upstream . lastBody , "tools.0.type" ). String ())
require . Equal ( t , "generate" , gjson . GetBytes ( upstream . lastBody , "tools.0.action" ). String ())
require . Equal ( t , "gpt-image-2" , gjson . GetBytes ( upstream . lastBody , "tools.0.model" ). String ())
require . Equal ( t , "1024x1024" , gjson . GetBytes ( upstream . lastBody , "tools.0.size" ). String ())
require . Equal ( t , "high" , gjson . GetBytes ( upstream . lastBody , "tools.0.quality" ). String ())
require . False ( t , gjson . GetBytes ( upstream . lastBody , "tools.0.n" ). Exists ())
require . Equal ( t , "draw a cat" , gjson . GetBytes ( upstream . lastBody , "input.0.content.0.text" ). String ())
require . Equal ( t , http . StatusOK , rec . Code )
require . Equal ( t , "aGVsbG8=" , gjson . Get ( rec . Body . String (), "data.0.b64_json" ). String ())
require . Equal ( t , "draw a cat" , gjson . Get ( rec . Body . String (), "data.0.revised_prompt" ). String ())
}
func TestOpenAIGatewayServiceForwardImages_OAuthStreamingTransformsEvents ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
body := [] byte ( `{"model":"gpt-image-2","prompt":"draw a cat","stream":true,"response_format":"url"}` )
req := httptest . NewRequest ( http . MethodPost , "/v1/images/generations" , bytes . NewReader ( body ))
req . Header . Set ( "Content-Type" , "application/json" )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
c . Request = req
svc := & OpenAIGatewayService {}
parsed , err := svc . ParseOpenAIImagesRequest ( c , body )
require . NoError ( t , err )
upstream := & httpUpstreamRecorder {
resp : & http . Response {
StatusCode : http . StatusOK ,
Header : http . Header {
"Content-Type" : [] string { "text/event-stream" },
"X-Request-Id" : [] string { "req_img_stream" },
},
Body : io . NopCloser ( strings . NewReader (
"data: {\"type\":\"response.image_generation_call.partial_image\",\"partial_image_b64\":\"cGFydGlhbA==\",\"partial_image_index\":0,\"output_format\":\"png\"}\n\n" +
"data: {\"type\":\"response.completed\",\"response\":{\"created_at\":1710000001,\"usage\":{\"input_tokens\":5,\"output_tokens\":9,\"output_tokens_details\":{\"image_tokens\":4}},\"tool_usage\":{\"image_gen\":{\"images\":1}},\"output\":[{\"type\":\"image_generation_call\",\"result\":\"ZmluYWw=\",\"output_format\":\"png\"}]}}\n\n" +
"data: [DONE]\n\n" ,
)),
},
}
svc . httpUpstream = upstream
account := & Account {
ID : 2 ,
Name : "openai-oauth" ,
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
Credentials : map [ string ] any {
"access_token" : "token-123" ,
},
}
result , err := svc . ForwardImages ( context . Background (), c , account , body , parsed , "" )
require . NoError ( t , err )
require . NotNil ( t , result )
require . True ( t , result . Stream )
require . Equal ( t , 1 , result . ImageCount )
require . Contains ( t , rec . Body . String (), "event: image_generation.partial_image" )
require . Contains ( t , rec . Body . String (), "event: image_generation.completed" )
require . Contains ( t , rec . Body . String (), "\"type\":\"image_generation.partial_image\"" )
require . Contains ( t , rec . Body . String (), "\"type\":\"image_generation.completed\"" )
require . Contains ( t , rec . Body . String (), "\"url\":\"data:image/png;base64,cGFydGlhbA==\"" )
require . Contains ( t , rec . Body . String (), "\"url\":\"data:image/png;base64,ZmluYWw=\"" )
}
func TestOpenAIGatewayServiceForwardImages_OAuthEditsMultipartUsesResponsesAPI ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
var body bytes . Buffer
writer := multipart . NewWriter ( & body )
require . NoError ( t , writer . WriteField ( "model" , "gpt-image-2" ))
require . NoError ( t , writer . WriteField ( "prompt" , "replace background with aurora" ))
require . NoError ( t , writer . WriteField ( "input_fidelity" , "high" ))
require . NoError ( t , writer . WriteField ( "output_format" , "webp" ))
require . NoError ( t , writer . WriteField ( "quality" , "high" ))
imageHeader := make ( textproto . MIMEHeader )
imageHeader . Set ( "Content-Disposition" , `form-data; name="image"; filename="source.png"` )
imageHeader . Set ( "Content-Type" , "image/png" )
imagePart , err := writer . CreatePart ( imageHeader )
require . NoError ( t , err )
_ , err = imagePart . Write ([] byte ( "png-image-content" ))
require . NoError ( t , err )
maskHeader := make ( textproto . MIMEHeader )
maskHeader . Set ( "Content-Disposition" , `form-data; name="mask"; filename="mask.png"` )
maskHeader . Set ( "Content-Type" , "image/png" )
maskPart , err := writer . CreatePart ( maskHeader )
require . NoError ( t , err )
_ , err = maskPart . Write ([] byte ( "png-mask-content" ))
require . NoError ( t , err )
require . NoError ( t , writer . Close ())
req := httptest . NewRequest ( http . MethodPost , "/v1/images/edits" , bytes . NewReader ( body . Bytes ()))
req . Header . Set ( "Content-Type" , writer . FormDataContentType ())
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
c . Request = req
c . Set ( "api_key" , & APIKey { ID : 100 })
svc := & OpenAIGatewayService {}
parsed , err := svc . ParseOpenAIImagesRequest ( c , body . Bytes ())
require . NoError ( t , err )
upstream := & httpUpstreamRecorder {
resp : & http . Response {
StatusCode : http . StatusOK ,
Header : http . Header {
"Content-Type" : [] string { "text/event-stream" },
"X-Request-Id" : [] string { "req_img_edit_123" },
},
Body : io . NopCloser ( strings . NewReader (
"data: {\"type\":\"response.completed\",\"response\":{\"created_at\":1710000002,\"usage\":{\"input_tokens\":13,\"output_tokens\":21,\"output_tokens_details\":{\"image_tokens\":8}},\"tool_usage\":{\"image_gen\":{\"images\":1}},\"output\":[{\"type\":\"image_generation_call\",\"result\":\"ZWRpdGVk\",\"revised_prompt\":\"replace background with aurora\",\"output_format\":\"webp\",\"quality\":\"high\"}]}}\n\n" +
"data: [DONE]\n\n" ,
)),
},
}
svc . httpUpstream = upstream
account := & Account {
ID : 3 ,
Name : "openai-oauth" ,
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
Credentials : map [ string ] any {
"access_token" : "token-123" ,
},
}
result , err := svc . ForwardImages ( context . Background (), c , account , body . Bytes (), parsed , "" )
require . NoError ( t , err )
require . NotNil ( t , result )
require . Equal ( t , 1 , result . ImageCount )
require . Equal ( t , "gpt-image-2" , gjson . GetBytes ( upstream . lastBody , "tools.0.model" ). String ())
require . Equal ( t , "edit" , gjson . GetBytes ( upstream . lastBody , "tools.0.action" ). String ())
require . Equal ( t , "high" , gjson . GetBytes ( upstream . lastBody , "tools.0.input_fidelity" ). String ())
require . Equal ( t , "webp" , gjson . GetBytes ( upstream . lastBody , "tools.0.output_format" ). String ())
require . True ( t , strings . HasPrefix ( gjson . GetBytes ( upstream . lastBody , "input.0.content.1.image_url" ). String (), "data:image/png;base64," ))
require . True ( t , strings . HasPrefix ( gjson . GetBytes ( upstream . lastBody , "tools.0.input_image_mask.image_url" ). String (), "data:image/png;base64," ))
require . Equal ( t , "replace background with aurora" , gjson . GetBytes ( upstream . lastBody , "input.0.content.0.text" ). String ())
require . Equal ( t , "ZWRpdGVk" , gjson . Get ( rec . Body . String (), "data.0.b64_json" ). String ())
require . Equal ( t , "replace background with aurora" , gjson . Get ( rec . Body . String (), "data.0.revised_prompt" ). String ())
}
func TestOpenAIGatewayServiceForwardImages_OAuthEditsStreamingTransformsEvents ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
body := [] byte ( `{
"model":"gpt-image-2",
"prompt":"replace background with aurora",
"images":[{"image_url":"https://example.com/source.png"}],
"mask":{"image_url":"https://example.com/mask.png"},
"stream":true,
"response_format":"url"
}` )
req := httptest . NewRequest ( http . MethodPost , "/v1/images/edits" , bytes . NewReader ( body ))
req . Header . Set ( "Content-Type" , "application/json" )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
c . Request = req
svc := & OpenAIGatewayService {}
parsed , err := svc . ParseOpenAIImagesRequest ( c , body )
require . NoError ( t , err )
upstream := & httpUpstreamRecorder {
resp : & http . Response {
StatusCode : http . StatusOK ,
Header : http . Header {
"Content-Type" : [] string { "text/event-stream" },
},
Body : io . NopCloser ( strings . NewReader (
"data: {\"type\":\"response.image_generation_call.partial_image\",\"partial_image_b64\":\"cGFydGlhbA==\",\"partial_image_index\":0,\"output_format\":\"webp\"}\n\n" +
"data: {\"type\":\"response.completed\",\"response\":{\"created_at\":1710000003,\"usage\":{\"input_tokens\":7,\"output_tokens\":10,\"output_tokens_details\":{\"image_tokens\":5}},\"tool_usage\":{\"image_gen\":{\"images\":1}},\"output\":[{\"type\":\"image_generation_call\",\"result\":\"ZWRpdGVk\",\"revised_prompt\":\"replace background with aurora\",\"output_format\":\"webp\"}]}}\n\n" +
"data: [DONE]\n\n" ,
)),
},
}
svc . httpUpstream = upstream
account := & Account {
ID : 4 ,
Name : "openai-oauth" ,
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
Credentials : map [ string ] any {
"access_token" : "token-123" ,
},
}
result , err := svc . ForwardImages ( context . Background (), c , account , body , parsed , "" )
require . NoError ( t , err )
require . NotNil ( t , result )
require . Equal ( t , 1 , result . ImageCount )
require . Equal ( t , "edit" , gjson . GetBytes ( upstream . lastBody , "tools.0.action" ). String ())
require . Equal ( t , "https://example.com/source.png" , gjson . GetBytes ( upstream . lastBody , "input.0.content.1.image_url" ). String ())
require . Equal ( t , "https://example.com/mask.png" , gjson . GetBytes ( upstream . lastBody , "tools.0.input_image_mask.image_url" ). String ())
require . Contains ( t , rec . Body . String (), "event: image_edit.partial_image" )
require . Contains ( t , rec . Body . String (), "event: image_edit.completed" )
require . Contains ( t , rec . Body . String (), "\"type\":\"image_edit.partial_image\"" )
require . Contains ( t , rec . Body . String (), "\"type\":\"image_edit.completed\"" )
require . Contains ( t , rec . Body . String (), "\"url\":\"data:image/webp;base64,cGFydGlhbA==\"" )
require . Contains ( t , rec . Body . String (), "\"url\":\"data:image/webp;base64,ZWRpdGVk\"" )
}
func TestBuildOpenAIImagesResponsesRequest_RejectsMultipleImages ( t * testing . T ) {
parsed := & OpenAIImagesRequest {
Endpoint : openAIImagesGenerationsEndpoint ,
Model : "gpt-image-2" ,
Prompt : "draw a cat" ,
N : 2 ,
}
body , err := buildOpenAIImagesResponsesRequest ( parsed , "gpt-image-2" )
require . Error ( t , err )
require . Nil ( t , body )
require . Contains ( t , err . Error (), "only n=1" )
}
func TestCollectOpenAIImagesFromResponsesBody_FallsBackToOutputItemDone ( t * testing . T ) {
body := [] byte (
"data: {\"type\":\"response.created\",\"response\":{\"created_at\":1710000004}}\n\n" +
"data: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"ig_123\",\"type\":\"image_generation_call\",\"result\":\"aGVsbG8=\",\"revised_prompt\":\"draw a cat\",\"output_format\":\"png\",\"quality\":\"high\"}}\n\n" +
"data: {\"type\":\"response.completed\",\"response\":{\"created_at\":1710000004,\"tool_usage\":{\"image_gen\":{\"images\":1}},\"output\":[]}}\n\n" +
"data: [DONE]\n\n" ,
)
results , createdAt , usageRaw , firstMeta , foundFinal , err := collectOpenAIImagesFromResponsesBody ( body )
require . NoError ( t , err )
require . True ( t , foundFinal )
require . Equal ( t , int64 ( 1710000004 ), createdAt )
require . Len ( t , results , 1 )
require . Equal ( t , "aGVsbG8=" , results [ 0 ]. Result )
require . Equal ( t , "draw a cat" , results [ 0 ]. RevisedPrompt )
require . Equal ( t , "png" , firstMeta . OutputFormat )
require . JSONEq ( t , `{"images":1}` , string ( usageRaw ))
}
func TestOpenAIGatewayServiceForwardImages_OAuthStreamingHandlesOutputItemDoneFallback ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
body := [] byte ( `{"model":"gpt-image-2","prompt":"draw a cat","stream":true,"response_format":"url"}` )
req := httptest . NewRequest ( http . MethodPost , "/v1/images/generations" , bytes . NewReader ( body ))
req . Header . Set ( "Content-Type" , "application/json" )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
c . Request = req
svc := & OpenAIGatewayService {}
parsed , err := svc . ParseOpenAIImagesRequest ( c , body )
require . NoError ( t , err )
upstream := & httpUpstreamRecorder {
resp : & http . Response {
StatusCode : http . StatusOK ,
Header : http . Header {
"Content-Type" : [] string { "text/event-stream" },
"X-Request-Id" : [] string { "req_img_stream_output_item_done" },
},
Body : io . NopCloser ( strings . NewReader (
"data: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"ig_123\",\"type\":\"image_generation_call\",\"result\":\"ZmluYWw=\",\"revised_prompt\":\"draw a cat\",\"output_format\":\"png\"}}\n\n" +
"data: {\"type\":\"response.completed\",\"response\":{\"created_at\":1710000005,\"usage\":{\"input_tokens\":5,\"output_tokens\":9,\"output_tokens_details\":{\"image_tokens\":4}},\"tool_usage\":{\"image_gen\":{\"images\":1}},\"output\":[]}}\n\n" +
"data: [DONE]\n\n" ,
)),
},
}
svc . httpUpstream = upstream
account := & Account {
ID : 5 ,
Name : "openai-oauth" ,
Platform : PlatformOpenAI ,
Type : AccountTypeOAuth ,
Credentials : map [ string ] any {
"access_token" : "token-123" ,
},
}
result , err := svc . ForwardImages ( context . Background (), c , account , body , parsed , "" )
require . NoError ( t , err )
require . NotNil ( t , result )
require . True ( t , result . Stream )
require . Equal ( t , 1 , result . ImageCount )
require . Contains ( t , rec . Body . String (), "event: image_generation.completed" )
require . Contains ( t , rec . Body . String (), "\"type\":\"image_generation.completed\"" )
require . Contains ( t , rec . Body . String (), "\"url\":\"data:image/png;base64,ZmluYWw=\"" )
require . NotContains ( t , rec . Body . String (), "event: error" )
}