Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
ca5d029e
Commit
ca5d029e
authored
Apr 28, 2026
by
VitalyR
Browse files
fix(openai): honor versioned image base URLs
parent
c92b88e3
Changes
3
Show whitespace changes
Inline
Side-by-side
backend/internal/service/account_test_service.go
View file @
ca5d029e
...
@@ -1227,7 +1227,7 @@ func (s *AccountTestService) testOpenAIImageAPIKey(c *gin.Context, ctx context.C
...
@@ -1227,7 +1227,7 @@ func (s *AccountTestService) testOpenAIImageAPIKey(c *gin.Context, ctx context.C
if
err
!=
nil
{
if
err
!=
nil
{
return
s
.
sendErrorAndEnd
(
c
,
fmt
.
Sprintf
(
"Invalid base URL: %s"
,
err
.
Error
()))
return
s
.
sendErrorAndEnd
(
c
,
fmt
.
Sprintf
(
"Invalid base URL: %s"
,
err
.
Error
()))
}
}
apiURL
:=
strings
.
TrimSuffix
(
normalizedBaseURL
,
"/"
)
+
"/v1/i
mages
/g
enerations
"
apiURL
:=
buildOpenAIImagesURL
(
normalizedBaseURL
,
openAII
mages
G
enerations
Endpoint
)
// Set SSE headers
// Set SSE headers
c
.
Writer
.
Header
()
.
Set
(
"Content-Type"
,
"text/event-stream"
)
c
.
Writer
.
Header
()
.
Set
(
"Content-Type"
,
"text/event-stream"
)
...
...
backend/internal/service/account_test_service_openai_image_test.go
View file @
ca5d029e
...
@@ -8,6 +8,7 @@ import (
...
@@ -8,6 +8,7 @@ import (
"strings"
"strings"
"testing"
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/require"
)
)
...
@@ -48,3 +49,42 @@ func TestAccountTestService_OpenAIImageOAuthHandlesOutputItemDoneFallback(t *tes
...
@@ -48,3 +49,42 @@ func TestAccountTestService_OpenAIImageOAuthHandlesOutputItemDoneFallback(t *tes
require
.
Contains
(
t
,
rec
.
Body
.
String
(),
"data:image/png;base64,aGVsbG8="
)
require
.
Contains
(
t
,
rec
.
Body
.
String
(),
"data:image/png;base64,aGVsbG8="
)
require
.
Contains
(
t
,
rec
.
Body
.
String
(),
"
\"
success
\"
:true"
)
require
.
Contains
(
t
,
rec
.
Body
.
String
(),
"
\"
success
\"
:true"
)
}
}
func
TestAccountTestService_OpenAIImageAPIKeyUsesConfiguredV1BaseURL
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
rec
:=
httptest
.
NewRecorder
()
c
,
_
:=
gin
.
CreateTestContext
(
rec
)
c
.
Request
=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/admin/accounts/1/test"
,
nil
)
upstream
:=
&
httpUpstreamRecorder
{
resp
:
&
http
.
Response
{
StatusCode
:
http
.
StatusOK
,
Header
:
http
.
Header
{
"Content-Type"
:
[]
string
{
"application/json"
},
},
Body
:
io
.
NopCloser
(
strings
.
NewReader
(
`{"data":[{"b64_json":"aGVsbG8=","revised_prompt":"draw a cat"}]}`
)),
},
}
svc
:=
&
AccountTestService
{
httpUpstream
:
upstream
,
cfg
:
&
config
.
Config
{},
}
account
:=
&
Account
{
ID
:
54
,
Name
:
"openai-apikey"
,
Platform
:
PlatformOpenAI
,
Type
:
AccountTypeAPIKey
,
Credentials
:
map
[
string
]
any
{
"api_key"
:
"test-api-key"
,
"base_url"
:
"https://image-upstream.example/v1"
,
},
}
err
:=
svc
.
testOpenAIImageAPIKey
(
c
,
context
.
Background
(),
account
,
"gpt-image-2"
,
"draw a cat"
)
require
.
NoError
(
t
,
err
)
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
.
Contains
(
t
,
rec
.
Body
.
String
(),
"data:image/png;base64,aGVsbG8="
)
require
.
Contains
(
t
,
rec
.
Body
.
String
(),
"
\"
success
\"
:true"
)
}
backend/internal/service/openai_images_test.go
View file @
ca5d029e
...
@@ -11,6 +11,7 @@ import (
...
@@ -11,6 +11,7 @@ import (
"strings"
"strings"
"testing"
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/tidwall/gjson"
...
@@ -258,6 +259,25 @@ func TestAccountSupportsOpenAIImageCapability_OAuthSupportsNative(t *testing.T)
...
@@ -258,6 +259,25 @@ func TestAccountSupportsOpenAIImageCapability_OAuthSupportsNative(t *testing.T)
require
.
True
(
t
,
account
.
SupportsOpenAIImageCapability
(
OpenAIImagesCapabilityNative
))
require
.
True
(
t
,
account
.
SupportsOpenAIImageCapability
(
OpenAIImagesCapabilityNative
))
}
}
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
),
)
}
type
openAIImageTestSSEEvent
struct
{
type
openAIImageTestSSEEvent
struct
{
Name
string
Name
string
Data
string
Data
string
...
@@ -371,6 +391,124 @@ func TestOpenAIGatewayServiceForwardImages_OAuthUsesResponsesAPI(t *testing.T) {
...
@@ -371,6 +391,124 @@ func TestOpenAIGatewayServiceForwardImages_OAuthUsesResponsesAPI(t *testing.T) {
require
.
Equal
(
t
,
"draw a cat"
,
gjson
.
Get
(
rec
.
Body
.
String
(),
"data.0.revised_prompt"
)
.
String
())
require
.
Equal
(
t
,
"draw a cat"
,
gjson
.
Get
(
rec
.
Body
.
String
(),
"data.0.revised_prompt"
)
.
String
())
}
}
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
())
}
func
TestOpenAIGatewayServiceForwardImages_OAuthStreamingTransformsEvents
(
t
*
testing
.
T
)
{
func
TestOpenAIGatewayServiceForwardImages_OAuthStreamingTransformsEvents
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
gin
.
SetMode
(
gin
.
TestMode
)
body
:=
[]
byte
(
`{"model":"gpt-image-2","prompt":"draw a cat","stream":true,"response_format":"url"}`
)
body
:=
[]
byte
(
`{"model":"gpt-image-2","prompt":"draw a cat","stream":true,"response_format":"url"}`
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment