| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480 |
- package openai_ws_v2
- import (
- "context"
- "errors"
- "io"
- "strings"
- "sync"
- "sync/atomic"
- "testing"
- "time"
- coderws "github.com/coder/websocket"
- "github.com/stretchr/testify/require"
- )
- type passthroughTestFrame struct {
- msgType coderws.MessageType
- payload []byte
- }
- type passthroughTestFrameConn struct {
- mu sync.Mutex
- writes []passthroughTestFrame
- readCh chan passthroughTestFrame
- once sync.Once
- }
- type delayedReadFrameConn struct {
- base FrameConn
- firstDelay time.Duration
- once sync.Once
- }
- type readStartSpyFrameConn struct {
- base FrameConn
- started chan struct{}
- startOnce sync.Once
- }
- type closeSpyFrameConn struct {
- closeCalls atomic.Int32
- }
- type eofReplacementFrameConn struct {
- FrameConn
- err error
- }
- type cancelJoinProbeFrameConn struct {
- readStarted chan struct{}
- readCanceled chan struct{}
- allowReturn chan struct{}
- readReturned chan struct{}
- startOnce sync.Once
- cancelOnce sync.Once
- returnOnce sync.Once
- }
- func newPassthroughTestFrameConn(frames []passthroughTestFrame, autoClose bool) *passthroughTestFrameConn {
- c := &passthroughTestFrameConn{
- readCh: make(chan passthroughTestFrame, len(frames)+1),
- }
- for _, frame := range frames {
- copied := passthroughTestFrame{msgType: frame.msgType, payload: append([]byte(nil), frame.payload...)}
- c.readCh <- copied
- }
- if autoClose {
- close(c.readCh)
- }
- return c
- }
- func (c *eofReplacementFrameConn) ReadFrame(ctx context.Context) (coderws.MessageType, []byte, error) {
- msgType, payload, err := c.FrameConn.ReadFrame(ctx)
- if errors.Is(err, io.EOF) {
- return msgType, payload, c.err
- }
- return msgType, payload, err
- }
- func (c *passthroughTestFrameConn) ReadFrame(ctx context.Context) (coderws.MessageType, []byte, error) {
- if ctx == nil {
- ctx = context.Background()
- }
- select {
- case <-ctx.Done():
- return coderws.MessageText, nil, ctx.Err()
- case frame, ok := <-c.readCh:
- if !ok {
- return coderws.MessageText, nil, io.EOF
- }
- return frame.msgType, append([]byte(nil), frame.payload...), nil
- }
- }
- func (c *passthroughTestFrameConn) WriteFrame(ctx context.Context, msgType coderws.MessageType, payload []byte) error {
- if ctx == nil {
- ctx = context.Background()
- }
- select {
- case <-ctx.Done():
- return ctx.Err()
- default:
- }
- c.mu.Lock()
- defer c.mu.Unlock()
- c.writes = append(c.writes, passthroughTestFrame{msgType: msgType, payload: append([]byte(nil), payload...)})
- return nil
- }
- func (c *passthroughTestFrameConn) Close() error {
- c.once.Do(func() {
- defer func() { _ = recover() }()
- close(c.readCh)
- })
- return nil
- }
- func (c *passthroughTestFrameConn) Writes() []passthroughTestFrame {
- c.mu.Lock()
- defer c.mu.Unlock()
- out := make([]passthroughTestFrame, len(c.writes))
- copy(out, c.writes)
- return out
- }
- func (c *delayedReadFrameConn) ReadFrame(ctx context.Context) (coderws.MessageType, []byte, error) {
- if c == nil || c.base == nil {
- return coderws.MessageText, nil, io.EOF
- }
- c.once.Do(func() {
- if c.firstDelay > 0 {
- timer := time.NewTimer(c.firstDelay)
- defer timer.Stop()
- select {
- case <-ctx.Done():
- case <-timer.C:
- }
- }
- })
- return c.base.ReadFrame(ctx)
- }
- func (c *delayedReadFrameConn) WriteFrame(ctx context.Context, msgType coderws.MessageType, payload []byte) error {
- if c == nil || c.base == nil {
- return io.EOF
- }
- return c.base.WriteFrame(ctx, msgType, payload)
- }
- func (c *delayedReadFrameConn) Close() error {
- if c == nil || c.base == nil {
- return nil
- }
- return c.base.Close()
- }
- func (c *readStartSpyFrameConn) ReadFrame(ctx context.Context) (coderws.MessageType, []byte, error) {
- c.startOnce.Do(func() { close(c.started) })
- return c.base.ReadFrame(ctx)
- }
- func (c *readStartSpyFrameConn) WriteFrame(ctx context.Context, msgType coderws.MessageType, payload []byte) error {
- return c.base.WriteFrame(ctx, msgType, payload)
- }
- func (c *readStartSpyFrameConn) Close() error {
- return c.base.Close()
- }
- func (c *closeSpyFrameConn) ReadFrame(ctx context.Context) (coderws.MessageType, []byte, error) {
- if ctx == nil {
- ctx = context.Background()
- }
- <-ctx.Done()
- return coderws.MessageText, nil, ctx.Err()
- }
- func (c *closeSpyFrameConn) WriteFrame(ctx context.Context, _ coderws.MessageType, _ []byte) error {
- if ctx == nil {
- ctx = context.Background()
- }
- select {
- case <-ctx.Done():
- return ctx.Err()
- default:
- return nil
- }
- }
- func (c *closeSpyFrameConn) Close() error {
- if c != nil {
- c.closeCalls.Add(1)
- }
- return nil
- }
- func (c *closeSpyFrameConn) CloseCalls() int32 {
- if c == nil {
- return 0
- }
- return c.closeCalls.Load()
- }
- func newCancelJoinProbeFrameConn() *cancelJoinProbeFrameConn {
- return &cancelJoinProbeFrameConn{
- readStarted: make(chan struct{}),
- readCanceled: make(chan struct{}),
- allowReturn: make(chan struct{}),
- readReturned: make(chan struct{}),
- }
- }
- func (c *cancelJoinProbeFrameConn) ReadFrame(ctx context.Context) (coderws.MessageType, []byte, error) {
- c.startOnce.Do(func() { close(c.readStarted) })
- <-ctx.Done()
- c.cancelOnce.Do(func() { close(c.readCanceled) })
- <-c.allowReturn
- c.returnOnce.Do(func() { close(c.readReturned) })
- return coderws.MessageText, nil, ctx.Err()
- }
- func (c *cancelJoinProbeFrameConn) WriteFrame(ctx context.Context, _ coderws.MessageType, _ []byte) error {
- if ctx == nil {
- ctx = context.Background()
- }
- select {
- case <-ctx.Done():
- return ctx.Err()
- default:
- return nil
- }
- }
- func (c *cancelJoinProbeFrameConn) Close() error { return nil }
- func TestRelay_BasicRelayAndUsage(t *testing.T) {
- t.Parallel()
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := newPassthroughTestFrameConn([]passthroughTestFrame{
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.completed","response":{"id":"resp_123","usage":{"input_tokens":7,"output_tokens":3,"input_tokens_details":{"cached_tokens":2}}}}`),
- },
- }, true)
- firstPayload := []byte(`{"type":"response.create","model":"gpt-5.3-codex","input":[{"type":"input_text","text":"hello"}]}`)
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
- result, relayExit := Relay(ctx, clientConn, upstreamConn, firstPayload, RelayOptions{})
- require.Nil(t, relayExit)
- require.Equal(t, "gpt-5.3-codex", result.RequestModel)
- require.Equal(t, "resp_123", result.RequestID)
- require.Equal(t, "response.completed", result.TerminalEventType)
- require.Equal(t, 7, result.Usage.InputTokens)
- require.Equal(t, 3, result.Usage.OutputTokens)
- require.Equal(t, 2, result.Usage.CacheReadInputTokens)
- 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)
- upstreamWrites := upstreamConn.Writes()
- require.Len(t, upstreamWrites, 1)
- require.Equal(t, coderws.MessageText, upstreamWrites[0].msgType)
- require.JSONEq(t, string(firstPayload), string(upstreamWrites[0].payload))
- clientWrites := clientConn.Writes()
- require.Len(t, clientWrites, 1)
- require.Equal(t, coderws.MessageText, clientWrites[0].msgType)
- require.JSONEq(t, `{"type":"response.completed","response":{"id":"resp_123","usage":{"input_tokens":7,"output_tokens":3,"input_tokens_details":{"cached_tokens":2}}}}`, string(clientWrites[0].payload))
- }
- func TestRelay_FunctionCallOutputBytesPreserved(t *testing.T) {
- t.Parallel()
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := newPassthroughTestFrameConn([]passthroughTestFrame{
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.completed","response":{"id":"resp_func","usage":{"input_tokens":1,"output_tokens":1}}}`),
- },
- }, true)
- firstPayload := []byte(`{"type":"response.create","model":"gpt-5.3-codex","input":[{"type":"function_call_output","call_id":"call_abc123","output":"{\"ok\":true}"}]}`)
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
- _, relayExit := Relay(ctx, clientConn, upstreamConn, firstPayload, RelayOptions{})
- require.Nil(t, relayExit)
- upstreamWrites := upstreamConn.Writes()
- require.Len(t, upstreamWrites, 1)
- require.Equal(t, coderws.MessageText, upstreamWrites[0].msgType)
- require.Equal(t, firstPayload, upstreamWrites[0].payload)
- }
- func TestRelay_UpstreamDisconnect(t *testing.T) {
- t.Parallel()
- // 上游立即关闭(EOF),客户端不发送额外帧
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := newPassthroughTestFrameConn(nil, true) // 立即 close -> EOF
- firstPayload := []byte(`{"type":"response.create","model":"gpt-4o","input":[]}`)
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
- result, relayExit := Relay(ctx, clientConn, upstreamConn, firstPayload, RelayOptions{})
- require.NotNil(t, relayExit)
- require.Equal(t, "read_upstream", relayExit.Stage)
- require.False(t, relayExit.Graceful)
- require.ErrorContains(t, relayExit.Err, "upstream websocket closed before terminal event")
- require.Equal(t, "gpt-4o", result.RequestModel)
- }
- func TestRelay_UpstreamNormalCloseBeforeResponseIDIsFailure(t *testing.T) {
- t.Parallel()
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := &eofReplacementFrameConn{
- FrameConn: newPassthroughTestFrameConn([]passthroughTestFrame{
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.created","status":"in_progress"}`),
- },
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.in_progress","status":"in_progress"}`),
- },
- }, true),
- err: coderws.CloseError{Code: coderws.StatusNormalClosure},
- }
- firstPayload := []byte(`{"type":"response.create","model":"gpt-5.6-sol","input":[]}`)
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
- result, relayExit := Relay(ctx, clientConn, upstreamConn, firstPayload, RelayOptions{})
- require.NotNil(t, relayExit)
- require.Equal(t, "read_upstream", relayExit.Stage)
- require.False(t, relayExit.Graceful)
- require.True(t, relayExit.WroteDownstream)
- require.ErrorContains(t, relayExit.Err, "upstream websocket closed before terminal event")
- require.Empty(t, result.RequestID)
- require.Empty(t, result.TerminalEventType)
- require.Len(t, clientConn.Writes(), 2)
- }
- func TestRelay_UpstreamNormalCloseBeforeTerminalIsFailure(t *testing.T) {
- t.Parallel()
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := &eofReplacementFrameConn{
- FrameConn: newPassthroughTestFrameConn([]passthroughTestFrame{
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.created","response":{"id":"resp_incomplete","status":"in_progress"}}`),
- },
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.in_progress","response":{"id":"resp_incomplete","status":"in_progress"}}`),
- },
- }, true),
- err: coderws.CloseError{Code: coderws.StatusNormalClosure},
- }
- firstPayload := []byte(`{"type":"response.create","model":"gpt-5.6-sol","input":[]}`)
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
- result, relayExit := Relay(ctx, clientConn, upstreamConn, firstPayload, RelayOptions{})
- require.NotNil(t, relayExit)
- require.Equal(t, "read_upstream", relayExit.Stage)
- require.False(t, relayExit.Graceful)
- require.True(t, relayExit.WroteDownstream)
- require.ErrorContains(t, relayExit.Err, "upstream websocket closed before terminal event")
- require.Empty(t, result.TerminalEventType)
- require.Len(t, clientConn.Writes(), 2)
- }
- func TestRelay_ClientDisconnect(t *testing.T) {
- t.Parallel()
- // 客户端立即关闭(EOF),上游阻塞读取直到 context 取消
- clientConn := newPassthroughTestFrameConn(nil, true) // 立即 close -> EOF
- upstreamConn := newPassthroughTestFrameConn(nil, false)
- firstPayload := []byte(`{"type":"response.create","model":"gpt-4o","input":[]}`)
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
- result, relayExit := Relay(ctx, clientConn, upstreamConn, firstPayload, RelayOptions{})
- require.NotNil(t, relayExit, "客户端 EOF 应返回可观测的中断状态")
- require.Equal(t, "client_disconnected", relayExit.Stage)
- require.Equal(t, "gpt-4o", result.RequestModel)
- }
- func TestRelay_ClientDisconnect_DrainCapturesLateUsage(t *testing.T) {
- t.Parallel()
- clientConn := newPassthroughTestFrameConn(nil, true)
- upstreamBase := newPassthroughTestFrameConn([]passthroughTestFrame{
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.completed","response":{"id":"resp_drain","usage":{"input_tokens":6,"output_tokens":4,"input_tokens_details":{"cached_tokens":1}}}}`),
- },
- }, true)
- upstreamConn := &delayedReadFrameConn{
- base: upstreamBase,
- firstDelay: 80 * time.Millisecond,
- }
- firstPayload := []byte(`{"type":"response.create","model":"gpt-4o","input":[]}`)
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
- result, relayExit := Relay(ctx, clientConn, upstreamConn, firstPayload, RelayOptions{
- UpstreamDrainTimeout: 400 * time.Millisecond,
- })
- require.NotNil(t, relayExit)
- require.Equal(t, "client_disconnected", relayExit.Stage)
- require.Equal(t, "resp_drain", result.RequestID)
- require.Equal(t, "response.completed", result.TerminalEventType)
- require.Equal(t, 6, result.Usage.InputTokens)
- require.Equal(t, 4, result.Usage.OutputTokens)
- require.Equal(t, 1, result.Usage.CacheReadInputTokens)
- require.Equal(t, int64(1), result.ClientToUpstreamFrames)
- require.Equal(t, int64(0), result.UpstreamToClientFrames)
- require.Equal(t, int64(1), result.DroppedDownstreamFrames)
- }
- func TestRelay_IdleTimeout(t *testing.T) {
- t.Parallel()
- // 客户端和上游都不发送帧,idle timeout 应触发
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := newPassthroughTestFrameConn(nil, false)
- firstPayload := []byte(`{"type":"response.create","model":"gpt-4o","input":[]}`)
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- // 使用快进时间来加速 idle timeout
- now := time.Now()
- callCount := 0
- nowFn := func() time.Time {
- callCount++
- // 前几次调用返回正常时间(初始化阶段),之后快进
- if callCount <= 5 {
- return now
- }
- return now.Add(time.Hour) // 快进到超时
- }
- result, relayExit := Relay(ctx, clientConn, upstreamConn, firstPayload, RelayOptions{
- IdleTimeout: 2 * time.Second,
- Now: nowFn,
- })
- require.NotNil(t, relayExit, "应因 idle timeout 退出")
- require.Equal(t, "idle_timeout", relayExit.Stage)
- require.Equal(t, "gpt-4o", result.RequestModel)
- }
- func TestRelay_IdleTimeoutDoesNotCloseClientOnError(t *testing.T) {
- t.Parallel()
- clientConn := &closeSpyFrameConn{}
- upstreamConn := &closeSpyFrameConn{}
- firstPayload := []byte(`{"type":"response.create","model":"gpt-4o","input":[]}`)
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- now := time.Now()
- callCount := 0
- nowFn := func() time.Time {
- callCount++
- if callCount <= 5 {
- return now
- }
- return now.Add(time.Hour)
- }
- _, relayExit := Relay(ctx, clientConn, upstreamConn, firstPayload, RelayOptions{
- IdleTimeout: 2 * time.Second,
- Now: nowFn,
- })
- require.NotNil(t, relayExit, "应因 idle timeout 退出")
- require.Equal(t, "idle_timeout", relayExit.Stage)
- require.Zero(t, clientConn.CloseCalls(), "错误路径不应提前关闭客户端连接,交给上层决定 close code")
- require.GreaterOrEqual(t, upstreamConn.CloseCalls(), int32(1))
- }
- func TestRelay_JoinsUpstreamReaderBeforeReturning(t *testing.T) {
- t.Parallel()
- clientConn := &closeSpyFrameConn{}
- upstreamConn := newCancelJoinProbeFrameConn()
- ctx, cancel := context.WithCancel(context.Background())
- resultCh := make(chan *RelayExit, 1)
- go func() {
- _, relayExit := Relay(
- ctx,
- clientConn,
- upstreamConn,
- []byte(`{"type":"response.create","model":"gpt-4o","input":[]}`),
- RelayOptions{},
- )
- resultCh <- relayExit
- }()
- select {
- case <-upstreamConn.readStarted:
- case <-time.After(time.Second):
- t.Fatal("upstream reader did not start")
- }
- cancel()
- select {
- case <-upstreamConn.readCanceled:
- case <-time.After(time.Second):
- t.Fatal("upstream reader did not observe relay cancellation")
- }
- select {
- case <-resultCh:
- t.Fatal("Relay returned before the upstream reader exited")
- case <-time.After(50 * time.Millisecond):
- }
- close(upstreamConn.allowReturn)
- select {
- case relayExit := <-resultCh:
- require.NotNil(t, relayExit)
- require.ErrorIs(t, relayExit.Err, context.Canceled)
- case <-time.After(time.Second):
- t.Fatal("Relay did not return after the upstream reader exited")
- }
- select {
- case <-upstreamConn.readReturned:
- default:
- t.Fatal("Relay returned before the upstream reader completion signal")
- }
- require.Zero(t, clientConn.CloseCalls(), "错误路径不应提前关闭客户端连接")
- }
- func TestRelay_NilConnections(t *testing.T) {
- t.Parallel()
- firstPayload := []byte(`{"type":"response.create","model":"gpt-4o","input":[]}`)
- ctx := context.Background()
- t.Run("nil client conn", func(t *testing.T) {
- upstreamConn := newPassthroughTestFrameConn(nil, true)
- _, relayExit := Relay(ctx, nil, upstreamConn, firstPayload, RelayOptions{})
- require.NotNil(t, relayExit)
- require.Equal(t, "relay_init", relayExit.Stage)
- require.Contains(t, relayExit.Err.Error(), "nil")
- })
- t.Run("nil upstream conn", func(t *testing.T) {
- clientConn := newPassthroughTestFrameConn(nil, true)
- _, relayExit := Relay(ctx, clientConn, nil, firstPayload, RelayOptions{})
- require.NotNil(t, relayExit)
- require.Equal(t, "relay_init", relayExit.Stage)
- require.Contains(t, relayExit.Err.Error(), "nil")
- })
- }
- func TestRelay_MultipleUpstreamMessages(t *testing.T) {
- t.Parallel()
- // 上游发送多个事件(delta + completed),验证多帧中继和 usage 聚合
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := newPassthroughTestFrameConn([]passthroughTestFrame{
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.output_text.delta","delta":"Hello"}`),
- },
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.output_text.delta","delta":" world"}`),
- },
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.completed","response":{"id":"resp_multi","usage":{"input_tokens":10,"output_tokens":5,"input_tokens_details":{"cached_tokens":3}}}}`),
- },
- }, true)
- firstPayload := []byte(`{"type":"response.create","model":"gpt-4o","input":[{"type":"input_text","text":"hi"}]}`)
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
- result, relayExit := Relay(ctx, clientConn, upstreamConn, firstPayload, RelayOptions{})
- require.Nil(t, relayExit)
- require.Equal(t, "resp_multi", result.RequestID)
- require.Equal(t, "response.completed", result.TerminalEventType)
- require.Equal(t, 10, result.Usage.InputTokens)
- require.Equal(t, 5, result.Usage.OutputTokens)
- require.Equal(t, 3, result.Usage.CacheReadInputTokens)
- require.NotNil(t, result.FirstTokenMs)
- // 验证所有 3 个上游帧都转发给了客户端
- clientWrites := clientConn.Writes()
- require.Len(t, clientWrites, 3)
- }
- func TestRelay_OnTurnComplete_PerTerminalEvent(t *testing.T) {
- t.Parallel()
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := newPassthroughTestFrameConn([]passthroughTestFrame{
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.completed","response":{"id":"resp_turn_1","usage":{"input_tokens":2,"output_tokens":1}}}`),
- },
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.failed","response":{"id":"resp_turn_2","usage":{"input_tokens":3,"output_tokens":4}}}`),
- },
- }, true)
- firstPayload := []byte(`{"type":"response.create","model":"gpt-5.3-codex","input":[]}`)
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
- turns := make([]RelayTurnResult, 0, 2)
- result, relayExit := Relay(ctx, clientConn, upstreamConn, firstPayload, RelayOptions{
- OnTurnComplete: func(turn RelayTurnResult) {
- turns = append(turns, turn)
- },
- })
- require.Nil(t, relayExit)
- require.Len(t, turns, 2)
- require.Equal(t, "resp_turn_1", turns[0].RequestID)
- require.Equal(t, "response.completed", turns[0].TerminalEventType)
- require.Equal(t, 2, turns[0].Usage.InputTokens)
- require.Equal(t, 1, turns[0].Usage.OutputTokens)
- require.Equal(t, "resp_turn_2", turns[1].RequestID)
- require.Equal(t, "response.failed", turns[1].TerminalEventType)
- require.Equal(t, 3, turns[1].Usage.InputTokens)
- require.Equal(t, 4, turns[1].Usage.OutputTokens)
- require.Equal(t, 5, result.Usage.InputTokens)
- require.Equal(t, 5, result.Usage.OutputTokens)
- }
- func TestRelay_OnTurnComplete_BareErrorWithoutIDBeforeLaterCompleted(t *testing.T) {
- t.Parallel()
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := newPassthroughTestFrameConn([]passthroughTestFrame{
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"error","usage":{"input_tokens":5,"output_tokens":1},"error":{"message":"first turn failed"}}`),
- },
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.completed","response":{"id":"resp_next","usage":{"input_tokens":3,"output_tokens":2}}}`),
- },
- }, true)
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
- turns := make([]RelayTurnResult, 0, 2)
- result, relayExit := Relay(
- ctx,
- clientConn,
- upstreamConn,
- []byte(`{"type":"response.create","model":"gpt-5.6-sol","input":[]}`),
- RelayOptions{OnTurnComplete: func(turn RelayTurnResult) { turns = append(turns, turn) }},
- )
- require.Nil(t, relayExit)
- require.Len(t, turns, 2)
- require.Equal(t, "error", turns[0].TerminalEventType)
- require.Empty(t, turns[0].RequestID)
- require.Equal(t, Usage{InputTokens: 5, OutputTokens: 1}, turns[0].Usage)
- require.Equal(t, "response.completed", turns[1].TerminalEventType)
- require.Equal(t, "resp_next", turns[1].RequestID)
- require.Equal(t, Usage{InputTokens: 3, OutputTokens: 2}, turns[1].Usage)
- require.Equal(t, Usage{InputTokens: 8, OutputTokens: 3}, result.Usage)
- }
- func TestRelay_OnTurnComplete_AuxiliaryFrameDoesNotSettleBareErrorBeforeFailed(t *testing.T) {
- t.Parallel()
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := newPassthroughTestFrameConn([]passthroughTestFrame{
- {msgType: coderws.MessageText, payload: []byte(`{"type":"error","error":{"code":"server_error","message":"failed"}}`)},
- {msgType: coderws.MessageText, payload: []byte(`{"type":"rate_limits.updated","rate_limits":[{"name":"requests","remaining":1}]}`)},
- {msgType: coderws.MessageText, payload: []byte(`{"type":"response.failed","response":{"id":"resp_authoritative","usage":{"input_tokens":7,"output_tokens":2},"error":{"code":"server_error","message":"failed"}}}`)},
- }, true)
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
- turns := make([]RelayTurnResult, 0, 1)
- result, relayExit := Relay(ctx, clientConn, upstreamConn, []byte(`{"type":"response.create","model":"gpt-5.6-sol","input":[]}`), RelayOptions{
- OnTurnComplete: func(turn RelayTurnResult) { turns = append(turns, turn) },
- })
- require.Nil(t, relayExit)
- require.Len(t, turns, 1)
- require.Equal(t, "response.failed", turns[0].TerminalEventType)
- require.Equal(t, "resp_authoritative", turns[0].RequestID)
- require.Equal(t, Usage{InputTokens: 7, OutputTokens: 2}, turns[0].Usage)
- require.Equal(t, turns[0].Usage, result.Usage)
- }
- func TestRelay_OnTurnComplete_UsesCurrentResponseCreateModel(t *testing.T) {
- t.Parallel()
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := newPassthroughTestFrameConn(nil, false)
- firstPayload := []byte(`{"type":"response.create","model":"gpt-5.6-sol","input":[]}`)
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
- turns := make(chan RelayTurnResult, 2)
- done := make(chan struct{})
- go func() {
- defer close(done)
- _, _ = Relay(ctx, clientConn, upstreamConn, firstPayload, RelayOptions{
- OnTurnComplete: func(turn RelayTurnResult) {
- turns <- turn
- },
- })
- }()
- upstreamConn.readCh <- passthroughTestFrame{
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.completed","response":{"id":"resp_sol","usage":{"input_tokens":2,"output_tokens":1}}}`),
- }
- firstTurn := <-turns
- require.Equal(t, "gpt-5.6-sol", firstTurn.RequestModel)
- clientConn.readCh <- passthroughTestFrame{
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.create","model":"gpt-5.6-terra","input":[]}`),
- }
- require.Eventually(t, func() bool {
- return len(upstreamConn.Writes()) == 2
- }, time.Second, 10*time.Millisecond)
- upstreamConn.readCh <- passthroughTestFrame{
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.completed","response":{"id":"resp_terra","usage":{"input_tokens":3,"output_tokens":1}}}`),
- }
- secondTurn := <-turns
- require.Equal(t, "gpt-5.6-terra", secondTurn.RequestModel)
- _ = clientConn.Close()
- _ = upstreamConn.Close()
- select {
- case <-done:
- case <-time.After(2 * time.Second):
- t.Fatal("relay did not stop after client close")
- }
- }
- func TestRelay_OnTurnComplete_ProvidesTurnMetrics(t *testing.T) {
- t.Parallel()
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := newPassthroughTestFrameConn([]passthroughTestFrame{
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.output_text.delta","response_id":"resp_metric","delta":"hi"}`),
- },
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.completed","response":{"id":"resp_metric","usage":{"input_tokens":2,"output_tokens":1}}}`),
- },
- }, true)
- firstPayload := []byte(`{"type":"response.create","model":"gpt-5.3-codex","input":[]}`)
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
- base := time.Unix(0, 0)
- var nowTick atomic.Int64
- nowFn := func() time.Time {
- step := nowTick.Add(1)
- return base.Add(time.Duration(step) * 5 * time.Millisecond)
- }
- var turn RelayTurnResult
- result, relayExit := Relay(ctx, clientConn, upstreamConn, firstPayload, RelayOptions{
- Now: nowFn,
- OnTurnComplete: func(current RelayTurnResult) {
- turn = current
- },
- })
- require.Nil(t, relayExit)
- require.Equal(t, "resp_metric", turn.RequestID)
- require.Equal(t, "response.completed", turn.TerminalEventType)
- require.NotNil(t, turn.FirstTokenMs)
- require.GreaterOrEqual(t, *turn.FirstTokenMs, 0)
- require.Greater(t, turn.Duration.Milliseconds(), int64(0))
- require.NotNil(t, result.FirstTokenMs)
- require.Greater(t, result.Duration.Milliseconds(), int64(0))
- }
- func TestRelay_OnTurnComplete_UsesResponseCreateTimeAcrossPricingBoundary(t *testing.T) {
- t.Parallel()
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := newPassthroughTestFrameConn([]passthroughTestFrame{
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.completed","response":{"id":"resp_boundary","usage":{"input_tokens":1,"output_tokens":1}}}`),
- },
- }, true)
- responseCreateAt := time.Date(2026, time.August, 17, 9, 59, 59, 0, time.UTC)
- upstreamResponseAt := responseCreateAt.Add(time.Second)
- var nowCalls atomic.Int64
- nowFn := func() time.Time {
- if nowCalls.Add(1) == 1 {
- return responseCreateAt
- }
- return upstreamResponseAt
- }
- var turn RelayTurnResult
- _, 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.Equal(t, responseCreateAt, turn.StartedAt)
- }
- func TestRelay_OnTurnComplete_UsesExplicitFirstTurnStartedAt(t *testing.T) {
- t.Parallel()
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := newPassthroughTestFrameConn([]passthroughTestFrame{
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.completed","response":{"id":"resp_initial_boundary","usage":{"input_tokens":1,"output_tokens":1}}}`),
- },
- }, true)
- responseCreateAt := time.Date(2026, time.August, 17, 9, 59, 59, 0, time.UTC)
- relayStartedAt := responseCreateAt.Add(time.Second)
- var turn RelayTurnResult
- _, relayExit := Relay(
- context.Background(),
- clientConn,
- upstreamConn,
- []byte(`{"type":"response.create","model":"gpt-5.3-codex","input":[]}`),
- RelayOptions{
- FirstTurnStartedAt: responseCreateAt,
- Now: func() time.Time { return relayStartedAt },
- OnTurnComplete: func(current RelayTurnResult) { turn = current },
- },
- )
- require.Nil(t, relayExit)
- require.Equal(t, responseCreateAt, turn.StartedAt)
- }
- func TestRelay_OnTurnComplete_UsesSubsequentResponseCreateTimeAcrossPricingBoundary(t *testing.T) {
- t.Parallel()
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := newPassthroughTestFrameConn(nil, false)
- firstTurnAt := time.Date(2026, time.August, 17, 9, 0, 0, 0, time.UTC)
- secondTurnAt := time.Date(2026, time.August, 17, 9, 59, 59, 0, time.UTC)
- secondResponseAt := secondTurnAt.Add(time.Second)
- var clock atomic.Int64
- clock.Store(firstTurnAt.UnixNano())
- turns := make(chan RelayTurnResult, 2)
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
- done := make(chan struct{})
- go func() {
- defer close(done)
- _, _ = Relay(
- ctx,
- clientConn,
- upstreamConn,
- []byte(`{"type":"response.create","model":"gpt-5.3-codex","input":[]}`),
- RelayOptions{
- Now: func() time.Time { return time.Unix(0, clock.Load()).UTC() },
- OnTurnComplete: func(current RelayTurnResult) {
- turns <- current
- },
- },
- )
- }()
- require.Eventually(t, func() bool { return len(upstreamConn.Writes()) == 1 }, time.Second, time.Millisecond)
- upstreamConn.readCh <- passthroughTestFrame{
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.completed","response":{"id":"resp_first","usage":{"input_tokens":1,"output_tokens":1}}}`),
- }
- select {
- case <-turns:
- case <-time.After(time.Second):
- t.Fatal("first turn did not complete")
- }
- clock.Store(secondTurnAt.UnixNano())
- clientConn.readCh <- passthroughTestFrame{
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.create","model":"gpt-5.3-codex","input":[]}`),
- }
- require.Eventually(t, func() bool { return len(upstreamConn.Writes()) == 2 }, time.Second, time.Millisecond)
- clock.Store(secondResponseAt.UnixNano())
- upstreamConn.readCh <- passthroughTestFrame{
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.completed","response":{"id":"resp_second","usage":{"input_tokens":1,"output_tokens":1}}}`),
- }
- var secondTurn RelayTurnResult
- select {
- case secondTurn = <-turns:
- case <-time.After(time.Second):
- t.Fatal("second turn did not complete")
- }
- require.Equal(t, secondTurnAt, secondTurn.StartedAt)
- cancel()
- select {
- case <-done:
- case <-time.After(time.Second):
- t.Fatal("relay did not stop after cancellation")
- }
- }
- func TestRelay_BeforeWriteClientTracksDownstreamPerTurn(t *testing.T) {
- t.Parallel()
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := newPassthroughTestFrameConn(nil, false)
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
- wroteStates := make(chan bool, 3)
- done := make(chan *RelayExit, 1)
- stopErr := errors.New("stop after second-turn error")
- go func() {
- _, relayExit := Relay(
- ctx,
- clientConn,
- upstreamConn,
- []byte(`{"type":"response.create","model":"gpt-5.3-codex","input":[]}`),
- RelayOptions{BeforeWriteClient: func(_ coderws.MessageType, payload []byte, wroteDownstream bool) error {
- wroteStates <- wroteDownstream
- if strings.Contains(string(payload), `"type":"error"`) {
- return stopErr
- }
- return nil
- }},
- )
- done <- relayExit
- }()
- require.Eventually(t, func() bool { return len(upstreamConn.Writes()) == 1 }, time.Second, time.Millisecond)
- upstreamConn.readCh <- passthroughTestFrame{
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.completed","response":{"id":"resp_first","usage":{"input_tokens":1,"output_tokens":1}}}`),
- }
- require.False(t, <-wroteStates)
- require.Eventually(t, func() bool { return len(clientConn.Writes()) == 1 }, time.Second, time.Millisecond)
- clientConn.readCh <- passthroughTestFrame{
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.create","model":"gpt-5.3-codex","input":[]}`),
- }
- require.Eventually(t, func() bool { return len(upstreamConn.Writes()) == 2 }, time.Second, time.Millisecond)
- upstreamConn.readCh <- passthroughTestFrame{
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"error","error":{"type":"usage_limit_reached"}}`),
- }
- require.False(t, <-wroteStates, "the next turn must not inherit the first turn's downstream write")
- select {
- case relayExit := <-done:
- require.NotNil(t, relayExit)
- require.ErrorIs(t, relayExit.Err, stopErr)
- require.True(t, relayExit.WroteDownstream, "connection-wide diagnostics must retain prior output")
- case <-time.After(time.Second):
- t.Fatal("relay did not stop after the rejected second-turn event")
- }
- }
- func TestRelay_BinaryFramePassthrough(t *testing.T) {
- t.Parallel()
- // 验证 binary frame 被透传但不进行 usage 解析
- binaryPayload := []byte{0x00, 0x01, 0x02, 0x03}
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := newPassthroughTestFrameConn([]passthroughTestFrame{
- {
- msgType: coderws.MessageBinary,
- payload: binaryPayload,
- },
- }, true)
- firstPayload := []byte(`{"type":"response.create","model":"gpt-4o","input":[]}`)
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
- result, relayExit := Relay(ctx, clientConn, upstreamConn, firstPayload, RelayOptions{})
- require.NotNil(t, relayExit)
- require.Equal(t, "read_upstream", relayExit.Stage)
- require.False(t, relayExit.Graceful)
- require.ErrorContains(t, relayExit.Err, "upstream websocket closed before terminal event")
- // binary frame 不解析 usage
- require.Equal(t, 0, result.Usage.InputTokens)
- clientWrites := clientConn.Writes()
- require.Len(t, clientWrites, 1)
- require.Equal(t, coderws.MessageBinary, clientWrites[0].msgType)
- require.Equal(t, binaryPayload, clientWrites[0].payload)
- }
- func TestRelay_BinaryJSONFrameSkipsObservation(t *testing.T) {
- t.Parallel()
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := newPassthroughTestFrameConn([]passthroughTestFrame{
- {
- msgType: coderws.MessageBinary,
- payload: []byte(`{"type":"response.completed","response":{"id":"resp_binary","usage":{"input_tokens":7,"output_tokens":3}}}`),
- },
- }, true)
- firstPayload := []byte(`{"type":"response.create","model":"gpt-4o","input":[]}`)
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
- result, relayExit := Relay(ctx, clientConn, upstreamConn, firstPayload, RelayOptions{})
- require.Nil(t, relayExit)
- require.Equal(t, 0, result.Usage.InputTokens)
- require.Equal(t, "", result.RequestID)
- require.Equal(t, "", result.TerminalEventType)
- clientWrites := clientConn.Writes()
- require.Len(t, clientWrites, 1)
- require.Equal(t, coderws.MessageBinary, clientWrites[0].msgType)
- }
- func TestRelay_UpstreamErrorEventPassthroughRaw(t *testing.T) {
- t.Parallel()
- clientConn := newPassthroughTestFrameConn(nil, false)
- errorEvent := []byte(`{"type":"error","error":{"type":"invalid_request_error","message":"No tool call found"}}`)
- upstreamConn := newPassthroughTestFrameConn([]passthroughTestFrame{
- {
- msgType: coderws.MessageText,
- payload: errorEvent,
- },
- }, true)
- firstPayload := []byte(`{"type":"response.create","model":"gpt-4o","input":[]}`)
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
- _, relayExit := Relay(ctx, clientConn, upstreamConn, firstPayload, RelayOptions{})
- require.Nil(t, relayExit)
- clientWrites := clientConn.Writes()
- require.Len(t, clientWrites, 1)
- require.Equal(t, coderws.MessageText, clientWrites[0].msgType)
- require.Equal(t, errorEvent, clientWrites[0].payload)
- }
- func TestRelay_PreservesFirstMessageType(t *testing.T) {
- t.Parallel()
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := newPassthroughTestFrameConn([]passthroughTestFrame{
- {
- msgType: coderws.MessageBinary,
- payload: []byte(`{"type":"response.completed","response":{"id":"resp_binary_first_type"}}`),
- },
- }, true)
- firstPayload := []byte(`{"type":"response.create","model":"gpt-4o","input":[]}`)
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
- _, relayExit := Relay(ctx, clientConn, upstreamConn, firstPayload, RelayOptions{
- FirstMessageType: coderws.MessageBinary,
- })
- require.Nil(t, relayExit)
- upstreamWrites := upstreamConn.Writes()
- require.Len(t, upstreamWrites, 1)
- require.Equal(t, coderws.MessageBinary, upstreamWrites[0].msgType)
- require.Equal(t, firstPayload, upstreamWrites[0].payload)
- clientWrites := clientConn.Writes()
- require.Len(t, clientWrites, 1)
- require.Equal(t, coderws.MessageBinary, clientWrites[0].msgType)
- }
- func TestRelay_UsageParseFailureDoesNotBlockRelay(t *testing.T) {
- baseline := SnapshotMetrics().UsageParseFailureTotal
- // 上游发送无效 JSON(非 usage 格式),不应影响透传
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := newPassthroughTestFrameConn([]passthroughTestFrame{
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.completed","response":{"id":"resp_bad","usage":"not_an_object"}}`),
- },
- }, true)
- firstPayload := []byte(`{"type":"response.create","model":"gpt-4o","input":[]}`)
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
- result, relayExit := Relay(ctx, clientConn, upstreamConn, firstPayload, RelayOptions{})
- require.Nil(t, relayExit)
- // usage 解析失败,值为 0 但不影响透传
- require.Equal(t, 0, result.Usage.InputTokens)
- require.Equal(t, "response.completed", result.TerminalEventType)
- // 帧仍然被转发
- clientWrites := clientConn.Writes()
- require.Len(t, clientWrites, 1)
- require.GreaterOrEqual(t, SnapshotMetrics().UsageParseFailureTotal, baseline+1)
- }
- func TestRelay_WriteUpstreamFirstMessageFails(t *testing.T) {
- t.Parallel()
- // 上游连接立即关闭,首包写入失败
- upstreamConn := newPassthroughTestFrameConn(nil, true)
- _ = upstreamConn.Close()
- // 覆盖 WriteFrame 使其返回错误
- errConn := &errorOnWriteFrameConn{}
- clientConn := newPassthroughTestFrameConn(nil, false)
- firstPayload := []byte(`{"type":"response.create","model":"gpt-4o","input":[]}`)
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
- _, relayExit := Relay(ctx, clientConn, errConn, firstPayload, RelayOptions{})
- require.NotNil(t, relayExit)
- require.Equal(t, "write_upstream", relayExit.Stage)
- }
- func TestRelay_ContextCanceled(t *testing.T) {
- t.Parallel()
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := newPassthroughTestFrameConn(nil, false)
- firstPayload := []byte(`{"type":"response.create","model":"gpt-4o","input":[]}`)
- // 立即取消 context
- ctx, cancel := context.WithCancel(context.Background())
- cancel()
- _, relayExit := Relay(ctx, clientConn, upstreamConn, firstPayload, RelayOptions{})
- // context 取消导致写首包失败
- require.NotNil(t, relayExit)
- }
- func TestRelay_DownstreamPreambleStartsClientReader(t *testing.T) {
- clientBase := newPassthroughTestFrameConn(nil, false)
- clientConn := &readStartSpyFrameConn{base: clientBase, started: make(chan struct{})}
- upstreamConn := newPassthroughTestFrameConn(nil, false)
- resultCh := make(chan *RelayExit, 1)
- ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
- defer cancel()
- go func() {
- _, relayExit := Relay(
- ctx,
- clientConn,
- upstreamConn,
- []byte(`{"type":"response.create","model":"gpt-5.1"}`),
- RelayOptions{
- StartClientAfterFirstDownstream: true,
- },
- )
- resultCh <- relayExit
- }()
- upstreamConn.readCh <- passthroughTestFrame{
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.created","response":{"id":"resp_semantic_gate"}}`),
- }
- require.Eventually(t, func() bool { return len(clientBase.Writes()) == 1 }, time.Second, 10*time.Millisecond)
- select {
- case <-clientConn.started:
- case <-time.After(time.Second):
- t.Fatal("response.created did not start the client reader")
- }
- upstreamConn.readCh <- passthroughTestFrame{
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.completed","response":{"id":"resp_semantic_gate","usage":{"input_tokens":1,"output_tokens":1}}}`),
- }
- _ = upstreamConn.Close()
- select {
- case relayExit := <-resultCh:
- require.Nil(t, relayExit)
- case <-time.After(time.Second):
- t.Fatal("relay did not finish after terminal event and upstream close")
- }
- }
- func TestRelay_TraceEvents_ContainsLifecycleStages(t *testing.T) {
- t.Parallel()
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := newPassthroughTestFrameConn([]passthroughTestFrame{
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.completed","response":{"id":"resp_trace","usage":{"input_tokens":1,"output_tokens":1}}}`),
- },
- }, true)
- firstPayload := []byte(`{"type":"response.create","model":"gpt-4o","input":[]}`)
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
- stages := make([]string, 0, 8)
- var stagesMu sync.Mutex
- _, relayExit := Relay(ctx, clientConn, upstreamConn, firstPayload, RelayOptions{
- OnTrace: func(event RelayTraceEvent) {
- stagesMu.Lock()
- stages = append(stages, event.Stage)
- stagesMu.Unlock()
- },
- })
- require.Nil(t, relayExit)
- stagesMu.Lock()
- capturedStages := append([]string(nil), stages...)
- stagesMu.Unlock()
- require.Contains(t, capturedStages, "relay_start")
- require.Contains(t, capturedStages, "write_first_message_ok")
- require.Contains(t, capturedStages, "first_exit")
- require.Contains(t, capturedStages, "relay_complete")
- }
- func TestRelay_TraceEvents_IdleTimeout(t *testing.T) {
- t.Parallel()
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := newPassthroughTestFrameConn(nil, false)
- firstPayload := []byte(`{"type":"response.create","model":"gpt-4o","input":[]}`)
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- now := time.Now()
- callCount := 0
- nowFn := func() time.Time {
- callCount++
- if callCount <= 5 {
- return now
- }
- return now.Add(time.Hour)
- }
- stages := make([]string, 0, 8)
- var stagesMu sync.Mutex
- _, relayExit := Relay(ctx, clientConn, upstreamConn, firstPayload, RelayOptions{
- IdleTimeout: 2 * time.Second,
- Now: nowFn,
- OnTrace: func(event RelayTraceEvent) {
- stagesMu.Lock()
- stages = append(stages, event.Stage)
- stagesMu.Unlock()
- },
- })
- require.NotNil(t, relayExit)
- require.Equal(t, "idle_timeout", relayExit.Stage)
- stagesMu.Lock()
- capturedStages := append([]string(nil), stages...)
- stagesMu.Unlock()
- require.Contains(t, capturedStages, "idle_timeout_triggered")
- require.Contains(t, capturedStages, "relay_exit")
- }
- // errorOnWriteFrameConn 是一个写入总是失败的 FrameConn 实现,用于测试首包写入失败。
- type errorOnWriteFrameConn struct{}
- func (c *errorOnWriteFrameConn) ReadFrame(ctx context.Context) (coderws.MessageType, []byte, error) {
- <-ctx.Done()
- return coderws.MessageText, nil, ctx.Err()
- }
- func (c *errorOnWriteFrameConn) WriteFrame(_ context.Context, _ coderws.MessageType, _ []byte) error {
- return errors.New("write failed: connection refused")
- }
- 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()
- clientConn := newPassthroughTestFrameConn(nil, false)
- upstreamConn := newPassthroughTestFrameConn([]passthroughTestFrame{
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.created","response":{"id":"resp_real"}}`),
- },
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.output_text.delta","delta":"He"}`),
- },
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.output_text.delta","delta":"llo"}`),
- },
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.output_text.delta","delta":" world"}`),
- },
- {
- msgType: coderws.MessageText,
- payload: []byte(`{"type":"response.completed","response":{"id":"resp_real","usage":{"input_tokens":2,"output_tokens":3}}}`),
- },
- }, true)
- firstPayload := []byte(`{"type":"response.create","model":"gpt-5.3-codex","input":[]}`)
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
- base := time.Unix(0, 0)
- var nowTick atomic.Int64
- nowFn := func() time.Time {
- step := nowTick.Add(1)
- return base.Add(time.Duration(step) * 10 * time.Millisecond)
- }
- var turn RelayTurnResult
- result, relayExit := Relay(ctx, clientConn, upstreamConn, firstPayload, RelayOptions{
- Now: nowFn,
- OnTurnComplete: func(current RelayTurnResult) {
- turn = current
- },
- })
- require.Nil(t, relayExit)
- require.Equal(t, "resp_real", turn.RequestID)
- require.Equal(t, "response.completed", turn.TerminalEventType)
- require.NotNil(t, turn.FirstTokenMs, "per-turn FirstTokenMs must be captured for real OpenAI streams")
- require.Greater(t, turn.Duration.Milliseconds(), int64(0))
- require.Less(t,
- int64(*turn.FirstTokenMs),
- turn.Duration.Milliseconds(),
- "per-turn FirstTokenMs (%dms) should be strictly less than Duration (%dms); "+
- "equality indicates the bug where first_token is mistakenly stamped on the terminal event",
- *turn.FirstTokenMs, turn.Duration.Milliseconds(),
- )
- require.NotNil(t, result.FirstTokenMs)
- require.Greater(t, *result.FirstTokenMs, 0)
- }
|