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"
2026-04-28 04:53:29 +08:00
"github.com/Wei-Shaw/sub2api/internal/config"
2026-04-22 12:30:08 +08:00
"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 ) )
}
2026-04-28 04:53:29 +08:00
func TestBuildOpenAIImagesURL_HandlesVersionedBaseURL ( t * testing . T ) {
require . Equal ( t ,
"https://image-upstream.example/v1/images/generations" ,
buildOpenAIImagesURL ( "https://image-upstream.example/v1" , openAIImagesGenerationsEndpoint ) ,
)
require . Equal ( t ,
"https://image-upstream.example/v1/images/edits" ,
buildOpenAIImagesURL ( "https://image-upstream.example/v1/" , openAIImagesEditsEndpoint ) ,
)
require . Equal ( t ,
"https://image-upstream.example/v1/images/generations" ,
buildOpenAIImagesURL ( "https://image-upstream.example" , openAIImagesGenerationsEndpoint ) ,
)
require . Equal ( t ,
"https://image-upstream.example/v1/images/generations" ,
buildOpenAIImagesURL ( "https://image-upstream.example/v1/images/generations" , openAIImagesGenerationsEndpoint ) ,
)
}
2026-04-23 15:09:47 +08:00
type openAIImageTestSSEEvent struct {
Name string
Data string
}
func parseOpenAIImageTestSSEEvents ( body string ) [ ] openAIImageTestSSEEvent {
chunks := strings . Split ( body , "\n\n" )
events := make ( [ ] openAIImageTestSSEEvent , 0 , len ( chunks ) )
for _ , chunk := range chunks {
chunk = strings . TrimSpace ( chunk )
if chunk == "" {
continue
}
var event openAIImageTestSSEEvent
for _ , line := range strings . Split ( chunk , "\n" ) {
switch {
case strings . HasPrefix ( line , "event: " ) :
event . Name = strings . TrimSpace ( strings . TrimPrefix ( line , "event: " ) )
case strings . HasPrefix ( line , "data: " ) :
event . Data = strings . TrimSpace ( strings . TrimPrefix ( line , "data: " ) )
}
}
if event . Name != "" || event . Data != "" {
events = append ( events , event )
}
}
return events
}
func findOpenAIImageTestSSEEvent ( events [ ] openAIImageTestSSEEvent , name string ) ( openAIImageTestSSEEvent , bool ) {
for _ , event := range events {
if event . Name == name {
return event , true
}
}
return openAIImageTestSSEEvent { } , false
}
2026-04-23 12:44:44 +08:00
func TestOpenAIGatewayServiceForwardImages_OAuthUsesResponsesAPI ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
2026-04-23 15:09:47 +08:00
body := [ ] byte ( ` { "model":"gpt-image-2","prompt":"draw a cat","size":"1024x1024","quality":"high","n":2} ` )
2026-04-23 12:44:44 +08:00
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 )
2026-04-23 15:09:47 +08:00
require . Equal ( t , "gpt-image-2" , gjson . Get ( rec . Body . String ( ) , "model" ) . String ( ) )
2026-04-23 12:44:44 +08:00
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 ( ) )
}
2026-04-28 04:53:29 +08:00
func TestOpenAIGatewayServiceForwardImages_APIKeyGenerationUsesConfiguredV1BaseURL ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
body := [ ] byte ( ` { "model":"gpt-image-2","prompt":"draw a cat","response_format":"b64_json"} ` )
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 {
cfg : & config . Config { } ,
httpUpstream : & httpUpstreamRecorder {
resp : & http . Response {
StatusCode : http . StatusOK ,
Header : http . Header {
"Content-Type" : [ ] string { "application/json" } ,
"X-Request-Id" : [ ] string { "req_img_apikey" } ,
} ,
Body : io . NopCloser ( strings . NewReader ( ` { "created":1710000007,"data":[ { "b64_json":"aGVsbG8=","revised_prompt":"draw a cat"}]} ` ) ) ,
} ,
} ,
}
parsed , err := svc . ParseOpenAIImagesRequest ( c , body )
require . NoError ( t , err )
account := & Account {
ID : 6 ,
Name : "openai-apikey" ,
Platform : PlatformOpenAI ,
Type : AccountTypeAPIKey ,
Credentials : map [ string ] any {
"api_key" : "test-api-key" ,
"base_url" : "https://image-upstream.example/v1" ,
} ,
}
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 , "gpt-image-2" , result . Model )
require . Equal ( t , "gpt-image-2" , result . UpstreamModel )
upstream , ok := svc . httpUpstream . ( * httpUpstreamRecorder )
require . True ( t , ok )
require . NotNil ( t , upstream . lastReq )
require . Equal ( t , "https://image-upstream.example/v1/images/generations" , upstream . lastReq . URL . String ( ) )
require . Equal ( t , "Bearer test-api-key" , upstream . lastReq . Header . Get ( "Authorization" ) )
require . Equal ( t , "application/json" , upstream . lastReq . Header . Get ( "Content-Type" ) )
require . Equal ( t , "gpt-image-2" , gjson . GetBytes ( upstream . lastBody , "model" ) . String ( ) )
require . Equal ( t , http . StatusOK , rec . Code )
require . Equal ( t , "aGVsbG8=" , gjson . Get ( rec . Body . String ( ) , "data.0.b64_json" ) . String ( ) )
}
func TestOpenAIGatewayServiceForwardImages_APIKeyEditUsesConfiguredV1BaseURL ( 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" ) )
imagePart , err := writer . CreateFormFile ( "image" , "source.png" )
require . NoError ( t , err )
_ , err = imagePart . Write ( [ ] byte ( "png-image-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
svc := & OpenAIGatewayService {
cfg : & config . Config { } ,
httpUpstream : & httpUpstreamRecorder {
resp : & http . Response {
StatusCode : http . StatusOK ,
Header : http . Header {
"Content-Type" : [ ] string { "application/json" } ,
"X-Request-Id" : [ ] string { "req_img_edit_apikey" } ,
} ,
Body : io . NopCloser ( strings . NewReader ( ` { "created":1710000008,"data":[ { "b64_json":"ZWRpdGVk","revised_prompt":"replace background"}]} ` ) ) ,
} ,
} ,
}
parsed , err := svc . ParseOpenAIImagesRequest ( c , body . Bytes ( ) )
require . NoError ( t , err )
account := & Account {
ID : 7 ,
Name : "openai-apikey" ,
Platform : PlatformOpenAI ,
Type : AccountTypeAPIKey ,
Credentials : map [ string ] any {
"api_key" : "test-api-key" ,
"base_url" : "https://image-upstream.example/v1/" ,
} ,
}
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 )
upstream , ok := svc . httpUpstream . ( * httpUpstreamRecorder )
require . True ( t , ok )
require . NotNil ( t , upstream . lastReq )
require . Equal ( t , "https://image-upstream.example/v1/images/edits" , upstream . lastReq . URL . String ( ) )
require . Equal ( t , "Bearer test-api-key" , upstream . lastReq . Header . Get ( "Authorization" ) )
require . Contains ( t , upstream . lastReq . Header . Get ( "Content-Type" ) , "multipart/form-data" )
require . Contains ( t , string ( upstream . lastBody ) , ` name="model" ` )
require . Contains ( t , string ( upstream . lastBody ) , "gpt-image-2" )
require . Equal ( t , http . StatusOK , rec . Code )
require . Equal ( t , "ZWRpdGVk" , gjson . Get ( rec . Body . String ( ) , "data.0.b64_json" ) . String ( ) )
}
2026-04-23 12:44:44 +08:00
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 (
2026-04-23 15:09:47 +08:00
"data: {\"type\":\"response.created\",\"response\":{\"created_at\":1710000001,\"tools\":[{\"type\":\"image_generation\",\"model\":\"gpt-image-2\",\"background\":\"auto\",\"output_format\":\"png\",\"quality\":\"high\",\"size\":\"1024x1024\"}]}}\n\n" +
"data: {\"type\":\"response.image_generation_call.partial_image\",\"partial_image_b64\":\"cGFydGlhbA==\",\"partial_image_index\":0,\"output_format\":\"png\",\"background\":\"auto\"}\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}},\"tools\":[{\"type\":\"image_generation\",\"model\":\"gpt-image-2\",\"background\":\"auto\",\"output_format\":\"png\",\"quality\":\"high\",\"size\":\"1024x1024\"}],\"output\":[{\"type\":\"image_generation_call\",\"result\":\"ZmluYWw=\",\"output_format\":\"png\"}]}}\n\n" +
2026-04-23 12:44:44 +08:00
"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 )
2026-04-23 15:09:47 +08:00
events := parseOpenAIImageTestSSEEvents ( rec . Body . String ( ) )
partial , ok := findOpenAIImageTestSSEEvent ( events , "image_generation.partial_image" )
require . True ( t , ok )
require . Equal ( t , "image_generation.partial_image" , gjson . Get ( partial . Data , "type" ) . String ( ) )
require . Equal ( t , int64 ( 1710000001 ) , gjson . Get ( partial . Data , "created_at" ) . Int ( ) )
require . Equal ( t , "cGFydGlhbA==" , gjson . Get ( partial . Data , "b64_json" ) . String ( ) )
require . Equal ( t , "data:image/png;base64,cGFydGlhbA==" , gjson . Get ( partial . Data , "url" ) . String ( ) )
require . Equal ( t , "gpt-image-2" , gjson . Get ( partial . Data , "model" ) . String ( ) )
require . Equal ( t , "png" , gjson . Get ( partial . Data , "output_format" ) . String ( ) )
require . Equal ( t , "high" , gjson . Get ( partial . Data , "quality" ) . String ( ) )
require . Equal ( t , "1024x1024" , gjson . Get ( partial . Data , "size" ) . String ( ) )
require . Equal ( t , "auto" , gjson . Get ( partial . Data , "background" ) . String ( ) )
completed , ok := findOpenAIImageTestSSEEvent ( events , "image_generation.completed" )
require . True ( t , ok )
require . Equal ( t , "image_generation.completed" , gjson . Get ( completed . Data , "type" ) . String ( ) )
require . Equal ( t , int64 ( 1710000001 ) , gjson . Get ( completed . Data , "created_at" ) . Int ( ) )
require . Equal ( t , "ZmluYWw=" , gjson . Get ( completed . Data , "b64_json" ) . String ( ) )
require . Equal ( t , "data:image/png;base64,ZmluYWw=" , gjson . Get ( completed . Data , "url" ) . String ( ) )
require . Equal ( t , "gpt-image-2" , gjson . Get ( completed . Data , "model" ) . String ( ) )
require . Equal ( t , "png" , gjson . Get ( completed . Data , "output_format" ) . String ( ) )
require . Equal ( t , "high" , gjson . Get ( completed . Data , "quality" ) . String ( ) )
require . Equal ( t , "1024x1024" , gjson . Get ( completed . Data , "size" ) . String ( ) )
require . Equal ( t , "auto" , gjson . Get ( completed . Data , "background" ) . String ( ) )
require . JSONEq ( t , ` { "images":1} ` , gjson . Get ( completed . Data , "usage" ) . Raw )
require . False ( t , gjson . Get ( completed . Data , "revised_prompt" ) . Exists ( ) )
2026-04-23 12:44:44 +08:00
}
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 ( ) )
2026-04-23 15:09:47 +08:00
require . False ( t , gjson . GetBytes ( upstream . lastBody , "tools.0.input_fidelity" ) . Exists ( ) )
2026-04-23 12:44:44 +08:00
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 (
2026-04-23 15:09:47 +08:00
"data: {\"type\":\"response.created\",\"response\":{\"created_at\":1710000003,\"tools\":[{\"type\":\"image_generation\",\"model\":\"gpt-image-2\",\"background\":\"transparent\",\"output_format\":\"webp\",\"quality\":\"high\",\"size\":\"1024x1024\"}]}}\n\n" +
"data: {\"type\":\"response.image_generation_call.partial_image\",\"partial_image_b64\":\"cGFydGlhbA==\",\"partial_image_index\":0,\"output_format\":\"webp\",\"background\":\"transparent\"}\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}},\"tools\":[{\"type\":\"image_generation\",\"model\":\"gpt-image-2\",\"background\":\"transparent\",\"output_format\":\"webp\",\"quality\":\"high\",\"size\":\"1024x1024\"}],\"output\":[{\"type\":\"image_generation_call\",\"result\":\"ZWRpdGVk\",\"revised_prompt\":\"replace background with aurora\",\"output_format\":\"webp\"}]}}\n\n" +
2026-04-23 12:44:44 +08:00
"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 ( ) )
2026-04-23 15:09:47 +08:00
events := parseOpenAIImageTestSSEEvents ( rec . Body . String ( ) )
partial , ok := findOpenAIImageTestSSEEvent ( events , "image_edit.partial_image" )
require . True ( t , ok )
require . Equal ( t , "image_edit.partial_image" , gjson . Get ( partial . Data , "type" ) . String ( ) )
require . Equal ( t , int64 ( 1710000003 ) , gjson . Get ( partial . Data , "created_at" ) . Int ( ) )
require . Equal ( t , "cGFydGlhbA==" , gjson . Get ( partial . Data , "b64_json" ) . String ( ) )
require . Equal ( t , "data:image/webp;base64,cGFydGlhbA==" , gjson . Get ( partial . Data , "url" ) . String ( ) )
require . Equal ( t , "gpt-image-2" , gjson . Get ( partial . Data , "model" ) . String ( ) )
require . Equal ( t , "webp" , gjson . Get ( partial . Data , "output_format" ) . String ( ) )
require . Equal ( t , "high" , gjson . Get ( partial . Data , "quality" ) . String ( ) )
require . Equal ( t , "1024x1024" , gjson . Get ( partial . Data , "size" ) . String ( ) )
require . Equal ( t , "transparent" , gjson . Get ( partial . Data , "background" ) . String ( ) )
completed , ok := findOpenAIImageTestSSEEvent ( events , "image_edit.completed" )
require . True ( t , ok )
require . Equal ( t , "image_edit.completed" , gjson . Get ( completed . Data , "type" ) . String ( ) )
require . Equal ( t , int64 ( 1710000003 ) , gjson . Get ( completed . Data , "created_at" ) . Int ( ) )
require . Equal ( t , "ZWRpdGVk" , gjson . Get ( completed . Data , "b64_json" ) . String ( ) )
require . Equal ( t , "data:image/webp;base64,ZWRpdGVk" , gjson . Get ( completed . Data , "url" ) . String ( ) )
require . Equal ( t , "gpt-image-2" , gjson . Get ( completed . Data , "model" ) . String ( ) )
require . Equal ( t , "webp" , gjson . Get ( completed . Data , "output_format" ) . String ( ) )
require . Equal ( t , "high" , gjson . Get ( completed . Data , "quality" ) . String ( ) )
require . Equal ( t , "1024x1024" , gjson . Get ( completed . Data , "size" ) . String ( ) )
require . Equal ( t , "transparent" , gjson . Get ( completed . Data , "background" ) . String ( ) )
require . JSONEq ( t , ` { "images":1} ` , gjson . Get ( completed . Data , "usage" ) . Raw )
require . False ( t , gjson . Get ( completed . Data , "revised_prompt" ) . Exists ( ) )
2026-04-23 12:44:44 +08:00
}
2026-04-23 15:09:47 +08:00
func TestBuildOpenAIImagesResponsesRequest_DowngradesMultipleImagesToSingle ( t * testing . T ) {
2026-04-23 12:44:44 +08:00
parsed := & OpenAIImagesRequest {
Endpoint : openAIImagesGenerationsEndpoint ,
Model : "gpt-image-2" ,
Prompt : "draw a cat" ,
N : 2 ,
}
body , err := buildOpenAIImagesResponsesRequest ( parsed , "gpt-image-2" )
2026-04-23 15:09:47 +08:00
require . NoError ( t , err )
require . NotNil ( t , body )
require . False ( t , gjson . GetBytes ( body , "tools.0.n" ) . Exists ( ) )
require . Equal ( t , "gpt-image-2" , gjson . GetBytes ( body , "tools.0.model" ) . String ( ) )
require . Equal ( t , "draw a cat" , gjson . GetBytes ( body , "input.0.content.0.text" ) . String ( ) )
}
func TestBuildOpenAIImagesResponsesRequest_StripsInputFidelity ( t * testing . T ) {
parsed := & OpenAIImagesRequest {
Endpoint : openAIImagesEditsEndpoint ,
Model : "gpt-image-2" ,
Prompt : "replace background" ,
InputFidelity : "high" ,
InputImageURLs : [ ] string {
"https://example.com/source.png" ,
} ,
}
body , err := buildOpenAIImagesResponsesRequest ( parsed , "gpt-image-2" )
require . NoError ( t , err )
require . NotNil ( t , body )
require . False ( t , gjson . GetBytes ( body , "tools.0.input_fidelity" ) . Exists ( ) )
require . Equal ( t , "edit" , gjson . GetBytes ( body , "tools.0.action" ) . String ( ) )
2026-04-23 12:44:44 +08:00
}
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 )
2026-04-23 15:09:47 +08:00
events := parseOpenAIImageTestSSEEvents ( rec . Body . String ( ) )
completed , ok := findOpenAIImageTestSSEEvent ( events , "image_generation.completed" )
require . True ( t , ok )
require . Equal ( t , "image_generation.completed" , gjson . Get ( completed . Data , "type" ) . String ( ) )
require . Equal ( t , int64 ( 1710000005 ) , gjson . Get ( completed . Data , "created_at" ) . Int ( ) )
require . Equal ( t , "ZmluYWw=" , gjson . Get ( completed . Data , "b64_json" ) . String ( ) )
require . Equal ( t , "data:image/png;base64,ZmluYWw=" , gjson . Get ( completed . Data , "url" ) . String ( ) )
require . Equal ( t , "gpt-image-2" , gjson . Get ( completed . Data , "model" ) . String ( ) )
require . JSONEq ( t , ` { "images":1} ` , gjson . Get ( completed . Data , "usage" ) . Raw )
2026-04-23 12:44:44 +08:00
require . NotContains ( t , rec . Body . String ( ) , "event: error" )
}