Merge pull request #5342 from wucm667/fix/issue-5340-ws-v2-terminal-ttft

fix(openai-ws): exclude terminal events from TTFT
This commit is contained in:
Wesley Liddick
2026-08-12 09:59:43 +08:00
committed by GitHub
3 changed files with 140 additions and 21 deletions
@@ -1023,23 +1023,10 @@ func shouldParseUsage(eventType string) bool {
}
func isTokenEvent(eventType string) bool {
if eventType == "" {
return false
}
switch eventType {
case "response.created", "response.in_progress", "response.output_item.added", "response.output_item.done":
return false
}
if strings.Contains(eventType, ".delta") {
return true
}
if strings.HasPrefix(eventType, "response.output_text") {
return true
}
if strings.HasPrefix(eventType, "response.output") {
return true
}
return eventType == "response.completed" || eventType == "response.done"
eventType = strings.TrimSpace(eventType)
return strings.HasSuffix(eventType, ".delta") ||
eventType == "response.output_text.done" ||
eventType == "response.function_call_arguments.done"
}
func minDuration(a, b time.Duration) time.Duration {
@@ -246,7 +246,7 @@ func TestHelperFunctionsCoverage(t *testing.T) {
require.True(t, isTokenEvent("response.output_text.delta"))
require.True(t, isTokenEvent("response.output_audio.delta"))
require.True(t, isTokenEvent("response.completed"))
require.False(t, isTokenEvent("response.completed"))
require.False(t, isTokenEvent(""))
require.False(t, isTokenEvent("response.created"))
@@ -407,8 +407,32 @@ func TestIsTokenEventCoverageBranches(t *testing.T) {
require.False(t, isTokenEvent("response.in_progress"))
require.False(t, isTokenEvent("response.output_item.added"))
require.True(t, isTokenEvent("response.output_audio.delta"))
require.True(t, isTokenEvent("response.output"))
require.True(t, isTokenEvent("response.done"))
require.True(t, isTokenEvent("response.function_call_arguments.delta"))
require.True(t, isTokenEvent("response.reasoning_summary_text.delta"))
require.True(t, isTokenEvent("response.output_text.done"))
require.True(t, isTokenEvent("response.function_call_arguments.done"))
require.False(t, isTokenEvent("response.output"))
require.False(t, isTokenEvent("response.output_audio.done"))
require.False(t, isTokenEvent("response.content_part.done"))
require.False(t, isTokenEvent("response.output_item.done"))
require.False(t, isTokenEvent("response.output_text.annotation.added"))
require.False(t, isTokenEvent("response.done"))
}
func TestTerminalAndTokenEventSetsAreDisjoint(t *testing.T) {
t.Parallel()
for _, eventType := range []string{
"response.completed",
"response.done",
"response.failed",
"response.incomplete",
"response.cancelled",
"response.canceled",
} {
require.True(t, isTerminalEvent(eventType), eventType)
require.False(t, isTokenEvent(eventType), eventType)
}
}
func TestShouldParseUsageTerminalEvents(t *testing.T) {
@@ -202,7 +202,7 @@ func TestRelay_BasicRelayAndUsage(t *testing.T) {
require.Equal(t, 7, result.Usage.InputTokens)
require.Equal(t, 3, result.Usage.OutputTokens)
require.Equal(t, 2, result.Usage.CacheReadInputTokens)
require.NotNil(t, result.FirstTokenMs)
require.Nil(t, result.FirstTokenMs)
require.Equal(t, int64(1), result.ClientToUpstreamFrames)
require.Equal(t, int64(1), result.UpstreamToClientFrames)
require.Equal(t, int64(0), result.DroppedDownstreamFrames)
@@ -865,6 +865,114 @@ func (c *errorOnWriteFrameConn) Close() error {
return nil
}
func TestRelay_NoSemanticOutputTerminalSequence_FirstTokenMsNil(t *testing.T) {
t.Parallel()
for _, terminalEvent := range []string{"response.completed", "response.done"} {
terminalEvent := terminalEvent
t.Run(terminalEvent, func(t *testing.T) {
t.Parallel()
clientConn := newPassthroughTestFrameConn(nil, false)
upstreamConn := newPassthroughTestFrameConn([]passthroughTestFrame{
{
msgType: coderws.MessageText,
payload: []byte(`{"type":"response.created","response":{"id":"resp_no_output"}}`),
},
{
msgType: coderws.MessageText,
payload: []byte(`{"type":"response.in_progress","response":{"id":"resp_no_output"}}`),
},
{
msgType: coderws.MessageText,
payload: []byte(`{"type":"response.content_part.done","response_id":"resp_no_output"}`),
},
{
msgType: coderws.MessageText,
payload: []byte(`{"type":"response.output_item.done","response_id":"resp_no_output"}`),
},
{
msgType: coderws.MessageText,
payload: []byte(`{"type":"` + terminalEvent + `","response":{"id":"resp_no_output","usage":{"input_tokens":2,"output_tokens":0}}}`),
},
}, true)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
var turn RelayTurnResult
result, relayExit := Relay(
ctx,
clientConn,
upstreamConn,
[]byte(`{"type":"response.create","model":"gpt-5.3-codex","input":[]}`),
RelayOptions{OnTurnComplete: func(current RelayTurnResult) { turn = current }},
)
require.Nil(t, relayExit)
require.Equal(t, terminalEvent, turn.TerminalEventType)
require.Nil(t, turn.FirstTokenMs)
require.Equal(t, terminalEvent, result.TerminalEventType)
require.Nil(t, result.FirstTokenMs)
require.Equal(t, int64(5), result.UpstreamToClientFrames)
})
}
}
func TestRelay_NoDeltaOutputDoneEvent_RecordsFirstTokenBeforeTerminal(t *testing.T) {
t.Parallel()
tests := []struct {
name string
donePayload string
}{
{
name: "output text done",
donePayload: `{"type":"response.output_text.done","response_id":"resp_done","text":"hello"}`,
},
{
name: "function call arguments done",
donePayload: `{"type":"response.function_call_arguments.done","response_id":"resp_done","arguments":"{\"city\":\"Paris\"}"}`,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
clientConn := newPassthroughTestFrameConn(nil, false)
upstreamConn := newPassthroughTestFrameConn([]passthroughTestFrame{
{msgType: coderws.MessageText, payload: []byte(`{"type":"response.created","response":{"id":"resp_done"}}`)},
{msgType: coderws.MessageText, payload: []byte(tt.donePayload)},
{msgType: coderws.MessageText, payload: []byte(`{"type":"response.completed","response":{"id":"resp_done","usage":{"input_tokens":2,"output_tokens":1}}}`)},
}, true)
base := time.Unix(0, 0)
var nowTick atomic.Int64
nowFn := func() time.Time {
return base.Add(time.Duration(nowTick.Add(1)) * 10 * time.Millisecond)
}
var turn RelayTurnResult
result, relayExit := Relay(
context.Background(),
clientConn,
upstreamConn,
[]byte(`{"type":"response.create","model":"gpt-5.3-codex","input":[]}`),
RelayOptions{
Now: nowFn,
OnTurnComplete: func(current RelayTurnResult) { turn = current },
},
)
require.Nil(t, relayExit)
require.NotNil(t, turn.FirstTokenMs)
require.Less(t, int64(*turn.FirstTokenMs), turn.Duration.Milliseconds())
require.NotNil(t, result.FirstTokenMs)
require.Less(t, int64(*result.FirstTokenMs), result.Duration.Milliseconds())
})
}
}
func TestRelay_OnTurnComplete_RealOpenAIStream_FirstTokenMs(t *testing.T) {
t.Parallel()