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
8987e0ba
Commit
8987e0ba
authored
Apr 25, 2026
by
hungryboy1025
Browse files
fix(openai): tighten responses stream account tests
parent
5d1c12e6
Changes
5
Show whitespace changes
Inline
Side-by-side
backend/internal/service/account_test_service.go
View file @
8987e0ba
...
@@ -1145,14 +1145,18 @@ func (s *AccountTestService) processClaudeStream(c *gin.Context, body io.Reader)
...
@@ -1145,14 +1145,18 @@ func (s *AccountTestService) processClaudeStream(c *gin.Context, body io.Reader)
// processOpenAIStream processes the SSE stream from OpenAI Responses API
// processOpenAIStream processes the SSE stream from OpenAI Responses API
func
(
s
*
AccountTestService
)
processOpenAIStream
(
c
*
gin
.
Context
,
body
io
.
Reader
)
error
{
func
(
s
*
AccountTestService
)
processOpenAIStream
(
c
*
gin
.
Context
,
body
io
.
Reader
)
error
{
reader
:=
bufio
.
NewReader
(
body
)
reader
:=
bufio
.
NewReader
(
body
)
seenCompleted
:=
false
for
{
for
{
line
,
err
:=
reader
.
ReadString
(
'\n'
)
line
,
err
:=
reader
.
ReadString
(
'\n'
)
if
err
!=
nil
{
if
err
!=
nil
{
if
err
==
io
.
EOF
{
if
err
==
io
.
EOF
{
if
seenCompleted
{
s
.
sendEvent
(
c
,
TestEvent
{
Type
:
"test_complete"
,
Success
:
true
})
s
.
sendEvent
(
c
,
TestEvent
{
Type
:
"test_complete"
,
Success
:
true
})
return
nil
return
nil
}
}
return
s
.
sendErrorAndEnd
(
c
,
"Stream ended before response.completed"
)
}
return
s
.
sendErrorAndEnd
(
c
,
fmt
.
Sprintf
(
"Stream read error: %s"
,
err
.
Error
()))
return
s
.
sendErrorAndEnd
(
c
,
fmt
.
Sprintf
(
"Stream read error: %s"
,
err
.
Error
()))
}
}
...
@@ -1163,9 +1167,12 @@ func (s *AccountTestService) processOpenAIStream(c *gin.Context, body io.Reader)
...
@@ -1163,9 +1167,12 @@ func (s *AccountTestService) processOpenAIStream(c *gin.Context, body io.Reader)
jsonStr
:=
sseDataPrefix
.
ReplaceAllString
(
line
,
""
)
jsonStr
:=
sseDataPrefix
.
ReplaceAllString
(
line
,
""
)
if
jsonStr
==
"[DONE]"
{
if
jsonStr
==
"[DONE]"
{
if
seenCompleted
{
s
.
sendEvent
(
c
,
TestEvent
{
Type
:
"test_complete"
,
Success
:
true
})
s
.
sendEvent
(
c
,
TestEvent
{
Type
:
"test_complete"
,
Success
:
true
})
return
nil
return
nil
}
}
return
s
.
sendErrorAndEnd
(
c
,
"Stream ended before response.completed"
)
}
var
data
map
[
string
]
any
var
data
map
[
string
]
any
if
err
:=
json
.
Unmarshal
([]
byte
(
jsonStr
),
&
data
);
err
!=
nil
{
if
err
:=
json
.
Unmarshal
([]
byte
(
jsonStr
),
&
data
);
err
!=
nil
{
...
@@ -1180,9 +1187,20 @@ func (s *AccountTestService) processOpenAIStream(c *gin.Context, body io.Reader)
...
@@ -1180,9 +1187,20 @@ func (s *AccountTestService) processOpenAIStream(c *gin.Context, body io.Reader)
if
delta
,
ok
:=
data
[
"delta"
]
.
(
string
);
ok
&&
delta
!=
""
{
if
delta
,
ok
:=
data
[
"delta"
]
.
(
string
);
ok
&&
delta
!=
""
{
s
.
sendEvent
(
c
,
TestEvent
{
Type
:
"content"
,
Text
:
delta
})
s
.
sendEvent
(
c
,
TestEvent
{
Type
:
"content"
,
Text
:
delta
})
}
}
case
"response.completed"
:
case
"response.completed"
,
"response.done"
:
seenCompleted
=
true
s
.
sendEvent
(
c
,
TestEvent
{
Type
:
"test_complete"
,
Success
:
true
})
s
.
sendEvent
(
c
,
TestEvent
{
Type
:
"test_complete"
,
Success
:
true
})
return
nil
return
nil
case
"response.failed"
:
errorMsg
:=
"OpenAI response failed"
if
responseData
,
ok
:=
data
[
"response"
]
.
(
map
[
string
]
any
);
ok
{
if
errData
,
ok
:=
responseData
[
"error"
]
.
(
map
[
string
]
any
);
ok
{
if
msg
,
ok
:=
errData
[
"message"
]
.
(
string
);
ok
&&
msg
!=
""
{
errorMsg
=
msg
}
}
}
return
s
.
sendErrorAndEnd
(
c
,
errorMsg
)
case
"error"
:
case
"error"
:
errorMsg
:=
"Unknown error"
errorMsg
:=
"Unknown error"
if
errData
,
ok
:=
data
[
"error"
]
.
(
map
[
string
]
any
);
ok
{
if
errData
,
ok
:=
data
[
"error"
]
.
(
map
[
string
]
any
);
ok
{
...
...
backend/internal/service/account_test_service_openai_test.go
View file @
8987e0ba
...
@@ -125,6 +125,31 @@ func TestAccountTestService_OpenAISuccessPersistsSnapshotFromHeaders(t *testing.
...
@@ -125,6 +125,31 @@ func TestAccountTestService_OpenAISuccessPersistsSnapshotFromHeaders(t *testing.
require
.
Contains
(
t
,
recorder
.
Body
.
String
(),
"test_complete"
)
require
.
Contains
(
t
,
recorder
.
Body
.
String
(),
"test_complete"
)
}
}
func
TestAccountTestService_OpenAIStreamEOFBeforeCompletedFails
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
ctx
,
recorder
:=
newTestContext
()
resp
:=
newJSONResponse
(
http
.
StatusOK
,
""
)
resp
.
Body
=
io
.
NopCloser
(
strings
.
NewReader
(
`data: {"type":"response.output_text.delta","delta":"hi"}
`
))
upstream
:=
&
queuedHTTPUpstream
{
responses
:
[]
*
http
.
Response
{
resp
}}
svc
:=
&
AccountTestService
{
httpUpstream
:
upstream
}
account
:=
&
Account
{
ID
:
90
,
Platform
:
PlatformOpenAI
,
Type
:
AccountTypeOAuth
,
Concurrency
:
1
,
Credentials
:
map
[
string
]
any
{
"access_token"
:
"test-token"
},
}
err
:=
svc
.
testOpenAIAccountConnection
(
ctx
,
account
,
"gpt-5.4"
,
""
,
""
)
require
.
Error
(
t
,
err
)
require
.
Contains
(
t
,
recorder
.
Body
.
String
(),
"response.completed"
)
require
.
NotContains
(
t
,
recorder
.
Body
.
String
(),
`"success":true`
)
}
func
TestAccountTestService_OpenAI429PersistsSnapshotAndRateLimitState
(
t
*
testing
.
T
)
{
func
TestAccountTestService_OpenAI429PersistsSnapshotAndRateLimitState
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
gin
.
SetMode
(
gin
.
TestMode
)
ctx
,
_
:=
newTestContext
()
ctx
,
_
:=
newTestContext
()
...
...
backend/internal/service/gateway_service.go
View file @
8987e0ba
...
@@ -119,7 +119,7 @@ func openAIStreamEventIsTerminal(data string) bool {
...
@@ -119,7 +119,7 @@ func openAIStreamEventIsTerminal(data string) bool {
return
true
return
true
}
}
switch
gjson
.
Get
(
trimmed
,
"type"
)
.
String
()
{
switch
gjson
.
Get
(
trimmed
,
"type"
)
.
String
()
{
case
"response.completed"
,
"response.done"
,
"response.failed"
:
case
"response.completed"
,
"response.done"
,
"response.failed"
,
"response.incomplete"
,
"response.cancelled"
,
"response.canceled"
:
return
true
return
true
default
:
default
:
return
false
return
false
...
...
backend/internal/service/openai_gateway_service.go
View file @
8987e0ba
...
@@ -4372,7 +4372,8 @@ func (s *OpenAIGatewayService) parseSSEUsageBytes(data []byte, usage *OpenAIUsag
...
@@ -4372,7 +4372,8 @@ func (s *OpenAIGatewayService) parseSSEUsageBytes(data []byte, usage *OpenAIUsag
return
return
}
}
eventType
:=
gjson
.
GetBytes
(
data
,
"type"
)
.
String
()
eventType
:=
gjson
.
GetBytes
(
data
,
"type"
)
.
String
()
if
eventType
!=
"response.completed"
&&
eventType
!=
"response.done"
{
if
eventType
!=
"response.completed"
&&
eventType
!=
"response.done"
&&
eventType
!=
"response.incomplete"
&&
eventType
!=
"response.cancelled"
&&
eventType
!=
"response.canceled"
{
return
return
}
}
...
@@ -4519,7 +4520,7 @@ func extractOpenAISSETerminalEvent(body string) (string, []byte, bool) {
...
@@ -4519,7 +4520,7 @@ func extractOpenAISSETerminalEvent(body string) (string, []byte, bool) {
}
}
eventType
:=
strings
.
TrimSpace
(
gjson
.
Get
(
data
,
"type"
)
.
String
())
eventType
:=
strings
.
TrimSpace
(
gjson
.
Get
(
data
,
"type"
)
.
String
())
switch
eventType
{
switch
eventType
{
case
"response.completed"
,
"response.done"
,
"response.failed"
:
case
"response.completed"
,
"response.done"
,
"response.failed"
,
"response.incomplete"
,
"response.cancelled"
,
"response.canceled"
:
return
eventType
,
[]
byte
(
data
),
true
return
eventType
,
[]
byte
(
data
),
true
}
}
}
}
...
...
backend/internal/service/openai_gateway_service_test.go
View file @
8987e0ba
...
@@ -1336,6 +1336,41 @@ func TestOpenAIStreamingPassthroughResponseDoneWithoutDoneMarkerStillSucceeds(t
...
@@ -1336,6 +1336,41 @@ func TestOpenAIStreamingPassthroughResponseDoneWithoutDoneMarkerStillSucceeds(t
require
.
Equal
(
t
,
1
,
result
.
usage
.
CacheReadInputTokens
)
require
.
Equal
(
t
,
1
,
result
.
usage
.
CacheReadInputTokens
)
}
}
func
TestOpenAIStreamingPassthroughResponseIncompleteWithoutDoneMarkerStillSucceeds
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
cfg
:=
&
config
.
Config
{
Gateway
:
config
.
GatewayConfig
{
MaxLineSize
:
defaultMaxLineSize
,
},
}
svc
:=
&
OpenAIGatewayService
{
cfg
:
cfg
}
rec
:=
httptest
.
NewRecorder
()
c
,
_
:=
gin
.
CreateTestContext
(
rec
)
c
.
Request
=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/"
,
nil
)
pr
,
pw
:=
io
.
Pipe
()
resp
:=
&
http
.
Response
{
StatusCode
:
http
.
StatusOK
,
Body
:
pr
,
Header
:
http
.
Header
{},
}
go
func
()
{
defer
func
()
{
_
=
pw
.
Close
()
}()
_
,
_
=
pw
.
Write
([]
byte
(
"data: {
\"
type
\"
:
\"
response.incomplete
\"
,
\"
response
\"
:{
\"
usage
\"
:{
\"
input_tokens
\"
:2,
\"
output_tokens
\"
:3,
\"
input_tokens_details
\"
:{
\"
cached_tokens
\"
:1}}}}
\n\n
"
))
}()
result
,
err
:=
svc
.
handleStreamingResponsePassthrough
(
c
.
Request
.
Context
(),
resp
,
c
,
&
Account
{
ID
:
1
},
time
.
Now
(),
""
,
""
)
_
=
pr
.
Close
()
require
.
NoError
(
t
,
err
)
require
.
NotNil
(
t
,
result
)
require
.
NotNil
(
t
,
result
.
usage
)
require
.
Equal
(
t
,
2
,
result
.
usage
.
InputTokens
)
require
.
Equal
(
t
,
3
,
result
.
usage
.
OutputTokens
)
require
.
Equal
(
t
,
1
,
result
.
usage
.
CacheReadInputTokens
)
}
func
TestOpenAIStreamingTooLong
(
t
*
testing
.
T
)
{
func
TestOpenAIStreamingTooLong
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
gin
.
SetMode
(
gin
.
TestMode
)
cfg
:=
&
config
.
Config
{
cfg
:=
&
config
.
Config
{
...
...
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