Merge pull request #4522 from sususu98/fix/responses-ws-continuity

This commit is contained in:
Luis Pater
2026-07-23 14:31:41 +08:00
13 changed files with 1987 additions and 238 deletions
@@ -1139,6 +1139,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
body = helps.SetStringIfDifferent(body, "model", baseModel)
body = helps.SetBoolIfDifferent(body, "stream", true)
body, _ = sjson.DeleteBytes(body, "previous_response_id")
body, _ = sjson.DeleteBytes(body, "generate")
body, _ = sjson.DeleteBytes(body, "prompt_cache_retention")
body, _ = sjson.DeleteBytes(body, "safety_identifier")
body, _ = sjson.DeleteBytes(body, "stream_options")
@@ -1408,6 +1409,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
requestPath := helps.PayloadRequestPath(opts)
body = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", body, originalTranslated, requestedModel, requestPath, opts.Headers)
body, _ = sjson.DeleteBytes(body, "previous_response_id")
body, _ = sjson.DeleteBytes(body, "generate")
body, _ = sjson.DeleteBytes(body, "prompt_cache_retention")
body, _ = sjson.DeleteBytes(body, "safety_identifier")
body, _ = sjson.DeleteBytes(body, "stream_options")
@@ -1580,6 +1582,7 @@ func (e *CodexExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth
body = helps.SetStringIfDifferent(body, "model", baseModel)
body, _ = sjson.DeleteBytes(body, "previous_response_id")
body, _ = sjson.DeleteBytes(body, "generate")
body, _ = sjson.DeleteBytes(body, "prompt_cache_retention")
body, _ = sjson.DeleteBytes(body, "safety_identifier")
body, _ = sjson.DeleteBytes(body, "stream_options")
@@ -352,6 +352,23 @@ func websocketSessionTargetChanged(sess *codexWebsocketSession, authID string, w
return strings.TrimSpace(sess.authID) != strings.TrimSpace(authID) || strings.TrimSpace(sess.wsURL) != strings.TrimSpace(wsURL)
}
func existingWebsocketSessionConn(sess *codexWebsocketSession, authID string, wsURL string) (*websocket.Conn, *websocketConnectionCloser) {
if sess == nil {
return nil, nil
}
sess.connMu.Lock()
conn := sess.conn
closer := sess.connCloser
matches := conn != nil && closer != nil &&
strings.TrimSpace(sess.authID) == strings.TrimSpace(authID) &&
strings.TrimSpace(sess.wsURL) == strings.TrimSpace(wsURL)
sess.connMu.Unlock()
if !matches || sess.upstreamDisconnectError(conn) != nil {
return nil, nil
}
return conn, closer
}
func detachMismatchedWebsocketSessionConn(sess *codexWebsocketSession, authID string, wsURL string) (*websocket.Conn, *websocketConnectionCloser, string, string, cliproxyexecutor.ExecutionLifecycle) {
if sess == nil {
return nil, nil, "", "", nil
@@ -532,14 +549,25 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
}
helps.RecordAPIWebsocketRequest(ctx, e.cfg, wsReqLog)
conn, closer, respHS, errDial := e.ensureUpstreamConn(ctx, auth, sess, authID, wsURL, wsHeaders)
var conn *websocket.Conn
var closer *websocketConnectionCloser
var respHS *http.Response
var errDial error
if cliproxyexecutor.RequiredUpstreamWebsocket(ctx) {
conn, closer = existingWebsocketSessionConn(sess, authID, wsURL)
if conn == nil {
return resp, cliproxyexecutor.NewUpstreamWebsocketReplayRequiredError()
}
} else {
conn, closer, respHS, errDial = e.ensureUpstreamConn(ctx, auth, sess, authID, wsURL, wsHeaders)
}
if errDial != nil {
bodyErr := websocketHandshakeBody(respHS)
if respHS != nil {
helps.RecordAPIWebsocketUpgradeRejection(ctx, e.cfg, websocketUpgradeRequestLog(wsReqLog), respHS.StatusCode, respHS.Header.Clone(), bodyErr)
}
if respHS != nil && respHS.StatusCode == http.StatusUpgradeRequired {
if opts.ExecutionLifecycle != nil {
if opts.ExecutionLifecycle != nil || cliproxyexecutor.DownstreamWebsocket(ctx) {
return resp, statusErr{code: respHS.StatusCode, msg: string(bodyErr)}
}
return e.CodexExecutor.Execute(ctx, auth, req, opts)
@@ -582,6 +610,14 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
if errSend := writeCodexWebsocketMessage(sess, conn, wsReqBody); errSend != nil {
errSend = mapCodexWebsocketWriteError(sess, conn, errSend)
if sess != nil {
if cliproxyexecutor.RequiredUpstreamWebsocket(ctx) {
e.invalidateUpstreamConnWithoutDisconnectNotify(sess, conn, "send_error", errSend)
if !shouldRetryCodexWebsocketSend(errSend) {
helps.RecordAPIWebsocketError(ctx, e.cfg, "send", errSend)
return resp, errSend
}
return resp, cliproxyexecutor.NewUpstreamWebsocketReplayRequiredError()
}
e.invalidateUpstreamConn(sess, conn, "send_error", errSend)
if !shouldRetryCodexWebsocketSend(errSend) {
helps.RecordAPIWebsocketError(ctx, e.cfg, "send", errSend)
@@ -810,7 +846,21 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
}
helps.RecordAPIWebsocketRequest(ctx, e.cfg, wsReqLog)
conn, closer, respHS, errDial := e.ensureUpstreamConn(ctx, auth, sess, authID, wsURL, wsHeaders)
var conn *websocket.Conn
var closer *websocketConnectionCloser
var respHS *http.Response
var errDial error
if cliproxyexecutor.RequiredUpstreamWebsocket(ctx) {
conn, closer = existingWebsocketSessionConn(sess, authID, wsURL)
if conn == nil {
if sess != nil {
sess.reqMu.Unlock()
}
return nil, cliproxyexecutor.NewUpstreamWebsocketReplayRequiredError()
}
} else {
conn, closer, respHS, errDial = e.ensureUpstreamConn(ctx, auth, sess, authID, wsURL, wsHeaders)
}
var upstreamHeaders http.Header
if respHS != nil {
upstreamHeaders = respHS.Header.Clone()
@@ -824,7 +874,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
if sess != nil {
sess.reqMu.Unlock()
}
if opts.ExecutionLifecycle != nil {
if opts.ExecutionLifecycle != nil || cliproxyexecutor.DownstreamWebsocket(ctx) {
return nil, statusErr{code: respHS.StatusCode, msg: string(bodyErr)}
}
return e.CodexExecutor.ExecuteStream(ctx, auth, req, opts)
@@ -864,6 +914,15 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
errSend = mapCodexWebsocketWriteError(sess, conn, errSend)
helps.RecordAPIWebsocketError(ctx, e.cfg, "send", errSend)
if sess != nil {
if cliproxyexecutor.RequiredUpstreamWebsocket(ctx) {
e.invalidateUpstreamConnWithoutDisconnectNotify(sess, conn, "send_error", errSend)
sess.clearActive(conn, readCh)
sess.reqMu.Unlock()
if !shouldRetryCodexWebsocketSend(errSend) {
return nil, errSend
}
return nil, cliproxyexecutor.NewUpstreamWebsocketReplayRequiredError()
}
e.invalidateUpstreamConn(sess, conn, "send_error", errSend)
if !shouldRetryCodexWebsocketSend(errSend) {
sess.clearActive(conn, readCh)
@@ -1964,6 +2023,14 @@ func (e *CodexWebsocketsExecutor) readUpstreamLoop(sess *codexWebsocketSession,
}
func (e *CodexWebsocketsExecutor) invalidateUpstreamConn(sess *codexWebsocketSession, conn *websocket.Conn, reason string, err error) {
e.invalidateUpstreamConnWithNotify(sess, conn, reason, err, true)
}
func (e *CodexWebsocketsExecutor) invalidateUpstreamConnWithoutDisconnectNotify(sess *codexWebsocketSession, conn *websocket.Conn, reason string, err error) {
e.invalidateUpstreamConnWithNotify(sess, conn, reason, err, false)
}
func (e *CodexWebsocketsExecutor) invalidateUpstreamConnWithNotify(sess *codexWebsocketSession, conn *websocket.Conn, reason string, err error, notify bool) {
if sess == nil || conn == nil {
return
}
@@ -1989,7 +2056,9 @@ func (e *CodexWebsocketsExecutor) invalidateUpstreamConn(sess *codexWebsocketSes
sess.connMu.Unlock()
logCodexWebsocketDisconnected(sessionID, authID, wsURL, reason, err)
sess.notifyUpstreamDisconnect(err)
if notify {
sess.notifyUpstreamDisconnect(err)
}
if closer != nil {
if errClose := closer.Close(); errClose != nil {
log.Errorf("codex websockets executor: close websocket error: %v", errClose)
@@ -2206,6 +2275,9 @@ func (e *CodexAutoExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
if cliproxyexecutor.DownstreamWebsocket(ctx) && codexWebsocketsEnabled(auth) {
return e.wsExec.Execute(ctx, auth, req, opts)
}
if cliproxyexecutor.RequiredUpstreamWebsocket(ctx) {
return cliproxyexecutor.Response{}, cliproxyexecutor.NewUpstreamWebsocketReplayRequiredError()
}
return e.httpExec.Execute(ctx, auth, req, opts)
}
@@ -2216,6 +2288,9 @@ func (e *CodexAutoExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
if cliproxyexecutor.DownstreamWebsocket(ctx) && codexWebsocketsEnabled(auth) {
return e.wsExec.ExecuteStream(ctx, auth, req, opts)
}
if cliproxyexecutor.RequiredUpstreamWebsocket(ctx) {
return nil, cliproxyexecutor.NewUpstreamWebsocketReplayRequiredError()
}
return e.httpExec.ExecuteStream(ctx, auth, req, opts)
}
@@ -361,6 +361,176 @@ func TestCodexWebsocketsExecutePreservesPreviousResponseIDUpstream(t *testing.T)
}
}
func TestCodexWebsocketsExecuteStreamUpgradeRequiredReturnsWithoutLockingSession(t *testing.T) {
upgradeAttempts := make(chan struct{}, 2)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.EqualFold(r.Header.Get("Upgrade"), "websocket") {
t.Errorf("unexpected HTTP fallback request: %s %s", r.Method, r.URL.Path)
w.WriteHeader(http.StatusInternalServerError)
return
}
upgradeAttempts <- struct{}{}
w.WriteHeader(http.StatusUpgradeRequired)
_, _ = w.Write([]byte(`{"error":{"message":"websocket unavailable"}}`))
}))
defer server.Close()
exec := NewCodexWebsocketsExecutor(&config.Config{SDKConfig: config.SDKConfig{DisableImageGeneration: config.DisableImageGenerationAll}})
const executionSessionID = "ws-upgrade-required-session"
t.Cleanup(func() { exec.CloseExecutionSession(executionSessionID) })
auth := &cliproxyauth.Auth{
ID: "codex-test",
Provider: "codex",
Attributes: map[string]string{
"api_key": "sk-test",
"base_url": server.URL,
},
}
opts := cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("openai-response"),
ResponseFormat: sdktranslator.FromString("openai-response"),
Metadata: map[string]any{
cliproxyexecutor.ExecutionSessionMetadataKey: executionSessionID,
},
}
ctx := cliproxyexecutor.WithDownstreamWebsocket(context.Background())
execute := func(payload string) {
t.Helper()
done := make(chan error, 1)
go func() {
_, errExecute := exec.ExecuteStream(ctx, auth, cliproxyexecutor.Request{
Model: "gpt-5.4",
Payload: []byte(payload),
}, opts)
done <- errExecute
}()
select {
case errExecute := <-done:
if errExecute == nil {
t.Fatal("upgrade-required error = nil")
}
statusErr, ok := errExecute.(interface{ StatusCode() int })
if !ok || statusErr.StatusCode() != http.StatusUpgradeRequired {
t.Fatalf("upgrade-required error = %T %v, want status 426", errExecute, errExecute)
}
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for upgrade-required error; execution session may still be locked")
}
}
execute(`{"model":"gpt-5.4","generate":false,"input":[]}`)
execute(`{"model":"gpt-5.4","previous_response_id":"resp-1","input":[{"type":"message","id":"msg-2"}]}`)
if got := len(upgradeAttempts); got != 2 {
t.Fatalf("websocket upgrade attempts = %d, want 2", got)
}
}
func TestCodexWebsocketsExecuteStreamHandshakeErrorReturnsWithoutLockingSession(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"error":{"message":"unauthorized"}}`))
}))
defer server.Close()
exec := NewCodexWebsocketsExecutor(&config.Config{SDKConfig: config.SDKConfig{DisableImageGeneration: config.DisableImageGenerationAll}})
const executionSessionID = "ws-handshake-error-session"
t.Cleanup(func() { exec.CloseExecutionSession(executionSessionID) })
auth := &cliproxyauth.Auth{
ID: "codex-test",
Provider: "codex",
Attributes: map[string]string{
"api_key": "sk-test",
"base_url": server.URL,
},
}
opts := cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("openai-response"),
Metadata: map[string]any{
cliproxyexecutor.ExecutionSessionMetadataKey: executionSessionID,
},
}
for i := 0; i < 2; i++ {
done := make(chan error, 1)
go func() {
_, errExecute := exec.ExecuteStream(context.Background(), auth, cliproxyexecutor.Request{
Model: "gpt-5.4",
Payload: []byte(`{"model":"gpt-5.4","input":[{"type":"message","id":"msg-1"}]}`),
}, opts)
done <- errExecute
}()
select {
case errExecute := <-done:
statusErr, ok := errExecute.(interface{ StatusCode() int })
if !ok || statusErr.StatusCode() != http.StatusUnauthorized {
t.Fatalf("attempt %d error = %T %v, want status 401", i+1, errExecute, errExecute)
}
case <-time.After(5 * time.Second):
t.Fatalf("attempt %d timed out; execution session remained locked", i+1)
}
}
}
func TestExistingWebsocketSessionConnRequiresMatchingHealthyConnection(t *testing.T) {
conn := &websocket.Conn{}
closer := newWebsocketConnectionCloser(conn)
sess := &codexWebsocketSession{
conn: conn,
connCloser: closer,
authID: "auth-a",
wsURL: "ws://example.test/responses",
}
sess.resetUpstreamDisconnectError(conn)
if gotConn, gotCloser := existingWebsocketSessionConn(sess, "auth-a", "ws://example.test/responses"); gotConn != conn || gotCloser != closer {
t.Fatal("matching healthy websocket session was not reusable")
}
if got, _ := existingWebsocketSessionConn(sess, "auth-b", "ws://example.test/responses"); got != nil {
t.Fatal("websocket session matched a different auth")
}
if got, _ := existingWebsocketSessionConn(sess, "auth-a", "ws://other.test/responses"); got != nil {
t.Fatal("websocket session matched a different URL")
}
sess.setUpstreamDisconnectError(conn, errors.New("upstream disconnected"))
if got, _ := existingWebsocketSessionConn(sess, "auth-a", "ws://example.test/responses"); got != nil {
t.Fatal("disconnected websocket session remained reusable")
}
}
func TestCodexAutoExecutorRequiredUpstreamWebsocketRejectsHTTPFallback(t *testing.T) {
exec := NewCodexAutoExecutor(&config.Config{SDKConfig: config.SDKConfig{DisableImageGeneration: config.DisableImageGenerationAll}})
auth := &cliproxyauth.Auth{
ID: "codex-http-only",
Provider: "codex",
Attributes: map[string]string{
"api_key": "sk-test",
},
}
ctx := cliproxyexecutor.WithRequiredUpstreamWebsocket(
cliproxyexecutor.WithDownstreamWebsocket(context.Background()),
)
_, errExecute := exec.ExecuteStream(ctx, auth, cliproxyexecutor.Request{
Model: "gpt-5.4",
Payload: []byte(`{"model":"gpt-5.4","previous_response_id":"resp-1","input":[{"type":"message","id":"msg-2"}]}`),
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("openai-response")})
if errExecute == nil {
t.Fatal("ExecuteStream() error = nil, want replay-required error")
}
statusErr, ok := errExecute.(interface{ StatusCode() int })
if !ok || statusErr.StatusCode() != http.StatusUpgradeRequired {
t.Fatalf("ExecuteStream() error = %T %v, want status 426", errExecute, errExecute)
}
if got := gjson.Get(errExecute.Error(), "error.code").String(); got != "upstream_http_replay_required" {
t.Fatalf("ExecuteStream() error code = %q, want upstream_http_replay_required", got)
}
requestScoped, ok := errExecute.(cliproxyexecutor.RequestScopedError)
if !ok || !requestScoped.IsRequestScoped() {
t.Fatalf("ExecuteStream() error = %T, want request-scoped replay signal", errExecute)
}
}
func TestCodexWebsocketsExecuteStreamPassesThroughUpstreamWebsocketPayloadForDownstreamWebsocket(t *testing.T) {
upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
capturedPayload := make(chan []byte, 1)
@@ -48,18 +48,21 @@ type xaiWebsocketIDStateStore struct {
}
type xaiWebsocketIDState struct {
mu sync.Mutex
downstreamToUpstream map[string]string
sequence int
transcriptInput []json.RawMessage
requestMu sync.Mutex
mu sync.Mutex
downstreamToUpstream map[string]string
sequence int
transcriptInput []json.RawMessage
replayCompactedTranscriptOnReset bool
}
type xaiWebsocketRequestIDMapper struct {
state *xaiWebsocketIDState
downstreamPreviousID string
upstreamPreviousID string
upstreamResponseID string
downstreamResponseID string
state *xaiWebsocketIDState
downstreamPreviousID string
upstreamPreviousID string
upstreamResponseID string
downstreamResponseID string
replayedCompactedTranscript bool
}
func NewXAIWebsocketsExecutor(cfg *config.Config) *XAIWebsocketsExecutor {
@@ -177,20 +180,21 @@ func (s *xaiWebsocketIDState) prependTranscriptInput(payload []byte) []byte {
return out
}
func (s *xaiWebsocketIDState) recordTranscriptTurn(requestPayload []byte, completedPayload []byte) {
func (s *xaiWebsocketIDState) recordTranscriptTurn(requestPayload []byte, completedPayload []byte, reset bool) {
if s == nil || len(requestPayload) == 0 || len(completedPayload) == 0 {
return
}
inputItems := xaiJSONRawMessages(gjson.GetBytes(requestPayload, "input"))
outputItems := xaiJSONRawMessages(gjson.GetBytes(completedPayload, "response.output"))
if len(inputItems) == 0 && len(outputItems) == 0 {
return
}
s.mu.Lock()
defer s.mu.Unlock()
if strings.TrimSpace(gjson.GetBytes(requestPayload, "previous_response_id").String()) == "" {
if reset {
s.transcriptInput = nil
s.replayCompactedTranscriptOnReset = false
}
if len(inputItems) == 0 && len(outputItems) == 0 {
return
}
s.transcriptInput = append(s.transcriptInput, inputItems...)
s.transcriptInput = append(s.transcriptInput, outputItems...)
@@ -210,9 +214,34 @@ func (s *xaiWebsocketIDState) replaceTranscriptWithItems(items ...[]byte) {
}
s.mu.Lock()
s.transcriptInput = next
s.replayCompactedTranscriptOnReset = len(next) > 0
s.mu.Unlock()
}
func (s *xaiWebsocketIDState) prependCompactedTranscriptOnReset(payload []byte) ([]byte, bool) {
if s == nil || len(payload) == 0 {
return payload, false
}
s.mu.Lock()
if !s.replayCompactedTranscriptOnReset || len(s.transcriptInput) == 0 {
s.mu.Unlock()
return payload, false
}
prefix := make([]json.RawMessage, 0, len(s.transcriptInput))
for _, item := range s.transcriptInput {
prefix = append(prefix, bytes.Clone(item))
}
s.mu.Unlock()
current := xaiJSONRawMessages(gjson.GetBytes(payload, "input"))
merged := append(prefix, current...)
out, errSet := sjson.SetRawBytes(payload, "input", xaiMarshalRawMessages(merged))
if errSet != nil {
return payload, false
}
return out, true
}
func xaiJSONRawMessages(result gjson.Result) []json.RawMessage {
if !result.Exists() || !result.IsArray() {
return nil
@@ -243,7 +272,16 @@ func xaiMarshalRawMessages(items []json.RawMessage) []byte {
}
func (m *xaiWebsocketRequestIDMapper) upstreamRequestPayload(payload []byte) []byte {
if m == nil || len(payload) == 0 || m.downstreamPreviousID == m.upstreamPreviousID {
if m == nil || len(payload) == 0 {
return payload
}
if m.downstreamPreviousID == m.upstreamPreviousID {
requestType := strings.TrimSpace(gjson.GetBytes(payload, "type").String())
if m.downstreamPreviousID == "" && requestType == "response.append" && m.state != nil {
out, replayed := m.state.prependCompactedTranscriptOnReset(payload)
m.replayedCompactedTranscript = replayed
return out
}
return payload
}
if m.upstreamPreviousID == "" {
@@ -251,6 +289,7 @@ func (m *xaiWebsocketRequestIDMapper) upstreamRequestPayload(payload []byte) []b
if errDelete == nil {
if m.downstreamPreviousID != "" && m.state != nil {
out = m.state.prependTranscriptInput(out)
m.replayedCompactedTranscript = true
}
return out
}
@@ -396,8 +435,30 @@ func (e *XAIWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *cliprox
if stateSessionID == "" {
stateSessionID = executionSessionID
}
idMapper := newXAIWebsocketRequestIDMapper(e.idStore, stateSessionID, req.Payload)
state := getXAIWebsocketIDState(e.idStore, stateSessionID)
stateRequestLocked := false
stateRequestLockTransferred := false
if executionSessionID == "" && state != nil {
state.requestMu.Lock()
stateRequestLocked = true
}
defer func() {
if stateRequestLocked && !stateRequestLockTransferred {
state.requestMu.Unlock()
}
}()
if xaiInputHasItemType(req.Payload, "compaction_trigger") {
if cliproxyexecutor.RequiredUpstreamWebsocket(ctx) {
return nil, cliproxyexecutor.NewUpstreamWebsocketReplayRequiredError()
}
if executionSessionID != "" {
sess := e.getOrCreateSession(executionSessionID)
if sess != nil {
sess.reqMu.Lock()
defer sess.reqMu.Unlock()
}
}
idMapper := newXAIWebsocketRequestIDMapper(e.idStore, stateSessionID, req.Payload)
return e.executeCompactionTriggerFromWebsocketContext(ctx, auth, req, opts, idMapper)
}
@@ -437,6 +498,7 @@ func (e *XAIWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *cliprox
sess.reqMu.Lock()
}
}
idMapper := newXAIWebsocketRequestIDMapper(e.idStore, stateSessionID, req.Payload)
if idMapper != nil {
if websocketSessionTargetChanged(sess, authID, wsURL) {
idMapper.upstreamPreviousID = ""
@@ -447,6 +509,9 @@ func (e *XAIWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *cliprox
wsHeaders := applyXAIWebsocketHeaders(http.Header{}, auth, token, prepared.sessionID)
wsReqBody := buildXAIWebsocketRequestBody(prepared.body)
requestType := strings.TrimSpace(gjson.GetBytes(req.Payload, "type").String())
transcriptReset := strings.TrimSpace(gjson.GetBytes(wsReqBody, "previous_response_id").String()) == "" &&
(requestType != "response.append" || (idMapper != nil && idMapper.replayedCompactedTranscript))
warmupRequest := xaiWebsocketGenerateFalse(wsReqBody)
wsReqLog := helps.UpstreamRequestLog{
@@ -463,7 +528,21 @@ func (e *XAIWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *cliprox
helps.RecordAPIWebsocketRequest(ctx, e.cfg, wsReqLog)
logXAIWebsocketRequest(executionSessionID, authID, wsURL, wsReqBody)
conn, closer, respHS, errDial := e.ensureUpstreamConn(ctx, auth, sess, authID, wsURL, wsHeaders)
var conn *websocket.Conn
var closer *websocketConnectionCloser
var respHS *http.Response
var errDial error
if cliproxyexecutor.RequiredUpstreamWebsocket(ctx) {
conn, closer = existingWebsocketSessionConn(sess, authID, wsURL)
if conn == nil {
if sess != nil {
sess.reqMu.Unlock()
}
return nil, cliproxyexecutor.NewUpstreamWebsocketReplayRequiredError()
}
} else {
conn, closer, respHS, errDial = e.ensureUpstreamConn(ctx, auth, sess, authID, wsURL, wsHeaders)
}
var upstreamHeaders http.Header
if respHS != nil {
upstreamHeaders = respHS.Header.Clone()
@@ -508,6 +587,15 @@ func (e *XAIWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *cliprox
errSend = mapXAIWebsocketWriteError(sess, conn, errSend)
helps.RecordAPIWebsocketError(ctx, e.cfg, "send", errSend)
if sess != nil {
if cliproxyexecutor.RequiredUpstreamWebsocket(ctx) {
e.invalidateUpstreamConnWithoutDisconnectNotify(sess, conn, "send_error", errSend)
sess.clearActive(conn, readCh)
sess.reqMu.Unlock()
if !shouldRetryXAIWebsocketSend(errSend) {
return nil, errSend
}
return nil, cliproxyexecutor.NewUpstreamWebsocketReplayRequiredError()
}
e.invalidateUpstreamConn(sess, conn, "send_error", errSend)
if !shouldRetryXAIWebsocketSend(errSend) {
sess.clearActive(conn, readCh)
@@ -570,7 +658,13 @@ func (e *XAIWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *cliprox
}
out := make(chan cliproxyexecutor.StreamChunk)
if stateRequestLocked {
stateRequestLockTransferred = true
}
go func() {
if stateRequestLocked {
defer state.requestMu.Unlock()
}
terminateReason := "completed"
var terminateErr error
@@ -677,6 +771,10 @@ func (e *XAIWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *cliprox
case "response.created":
if warmupRequest {
warmupCompletedPayload = buildXAIWebsocketWarmupCompletedPayload(payload)
if idMapper != nil && idMapper.state != nil && !recordedTranscript {
idMapper.state.recordTranscriptTurn(wsReqBody, warmupCompletedPayload, transcriptReset)
recordedTranscript = true
}
logXAIWebsocketWarmupCompleted(executionSessionID, authID, wsURL, payload)
}
case "response.output_item.done":
@@ -690,7 +788,7 @@ func (e *XAIWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *cliprox
payload = xaiNormalizeReasoningSummaryData(payload)
cacheXAIReasoningReplayFromCompleted(ctx, prepared.replayScope, payload)
if !warmupRequest && idMapper != nil && idMapper.state != nil && !recordedTranscript {
idMapper.state.recordTranscriptTurn(wsReqBody, payload)
idMapper.state.recordTranscriptTurn(wsReqBody, payload, transcriptReset)
recordedTranscript = true
}
case "response.done":
@@ -699,7 +797,7 @@ func (e *XAIWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *cliprox
reporter.Publish(ctx, detail)
}
if !warmupRequest && idMapper != nil && idMapper.state != nil && !recordedTranscript {
idMapper.state.recordTranscriptTurn(wsReqBody, payload)
idMapper.state.recordTranscriptTurn(wsReqBody, payload, transcriptReset)
recordedTranscript = true
}
}
@@ -793,8 +891,11 @@ func (e *XAIWebsocketsExecutor) executeCompactionTriggerFromWebsocketContext(ctx
return nil, err
}
responseID := xaiCompactionResponseID(data)
idMapper.state.replaceTranscriptWithItems(xaiCompactionOutputItem(data, responseID))
responseID, compactionItem, errValidate := validateXAIWebsocketCompactionResponse(data)
if errValidate != nil {
return nil, errValidate
}
idMapper.state.replaceTranscriptWithItems(compactionItem)
idMapper.state.mapDownstreamToUpstream(responseID, "")
headers = headers.Clone()
@@ -812,6 +913,30 @@ func (e *XAIWebsocketsExecutor) executeCompactionTriggerFromWebsocketContext(ctx
return &cliproxyexecutor.StreamResult{Headers: headers, Chunks: out}, nil
}
func validateXAIWebsocketCompactionResponse(data []byte) (string, []byte, error) {
if len(data) == 0 || !json.Valid(data) {
return "", nil, statusErr{code: http.StatusBadGateway, msg: "xai websocket compaction returned invalid JSON"}
}
responseIDResult := gjson.GetBytes(data, "id")
output := gjson.GetBytes(data, "output")
if responseIDResult.Type != gjson.String || strings.TrimSpace(responseIDResult.String()) == "" || !output.Exists() || !output.IsArray() {
return "", nil, statusErr{code: http.StatusBadGateway, msg: "xai websocket compaction response is missing compacted state"}
}
items := output.Array()
if len(items) == 0 {
return "", nil, statusErr{code: http.StatusBadGateway, msg: "xai websocket compaction response is missing compacted state"}
}
item := items[0]
itemType := item.Get("type")
encryptedContent := item.Get("encrypted_content")
if item.Type != gjson.JSON || itemType.Type != gjson.String || strings.TrimSpace(itemType.String()) != "compaction" ||
encryptedContent.Type != gjson.String || strings.TrimSpace(encryptedContent.String()) == "" {
return "", nil, statusErr{code: http.StatusBadGateway, msg: "xai websocket compaction response is missing compacted state"}
}
normalizedResponseID := xaiCompactionResponseID(data)
return normalizedResponseID, xaiCompactionOutputItem(data, normalizedResponseID), nil
}
func buildXAIWebsocketCompactionPayload(payload []byte, transcriptInput []byte) ([]byte, error) {
if len(payload) == 0 {
payload = []byte(`{}`)
@@ -1497,6 +1622,9 @@ func (e *XAIAutoExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.
if cliproxyexecutor.DownstreamWebsocket(ctx) && xaiWebsocketsEnabled(auth) {
return e.wsExec.ExecuteStream(ctx, auth, req, opts)
}
if cliproxyexecutor.RequiredUpstreamWebsocket(ctx) {
return nil, cliproxyexecutor.NewUpstreamWebsocketReplayRequiredError()
}
return e.httpExec.ExecuteStream(ctx, auth, req, opts)
}
@@ -34,6 +34,50 @@ func TestXAIWebsocketsEnabledForConfigAPIKey(t *testing.T) {
}
}
func TestXAIAutoExecutorRequiredUpstreamWebsocketRejectsHTTPFallback(t *testing.T) {
exec := NewXAIAutoExecutor(&config.Config{})
auth := &cliproxyauth.Auth{
ID: "xai-http-only",
Provider: "xai",
Attributes: map[string]string{
"api_key": "xai-key",
},
}
ctx := cliproxyexecutor.WithRequiredUpstreamWebsocket(
cliproxyexecutor.WithDownstreamWebsocket(context.Background()),
)
_, errExecute := exec.ExecuteStream(ctx, auth, cliproxyexecutor.Request{
Model: "grok-4",
Payload: []byte(`{"model":"grok-4","previous_response_id":"resp-1","input":[{"type":"message","id":"msg-2"}]}`),
}, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("openai-response")})
if errExecute == nil {
t.Fatal("ExecuteStream() error = nil, want replay-required error")
}
statusErr, ok := errExecute.(interface{ StatusCode() int })
if !ok || statusErr.StatusCode() != http.StatusUpgradeRequired {
t.Fatalf("ExecuteStream() error = %T %v, want status 426", errExecute, errExecute)
}
if got := gjson.Get(errExecute.Error(), "error.code").String(); got != "upstream_http_replay_required" {
t.Fatalf("ExecuteStream() error code = %q, want upstream_http_replay_required", got)
}
requestScoped, ok := errExecute.(cliproxyexecutor.RequestScopedError)
if !ok || !requestScoped.IsRequestScoped() {
t.Fatalf("ExecuteStream() error = %T, want request-scoped replay signal", errExecute)
}
}
func TestXAIWebsocketsRequiredUpstreamRejectsCompactionHTTPFallback(t *testing.T) {
exec := NewXAIWebsocketsExecutor(&config.Config{})
ctx := cliproxyexecutor.WithRequiredUpstreamWebsocket(context.Background())
_, errExecute := exec.ExecuteStream(ctx, &cliproxyauth.Auth{}, cliproxyexecutor.Request{
Model: "grok-4",
Payload: []byte(`{"model":"grok-4","input":[{"type":"compaction_trigger"}]}`),
}, cliproxyexecutor.Options{})
if !cliproxyexecutor.IsUpstreamWebsocketReplayRequired(errExecute) {
t.Fatalf("ExecuteStream() error = %T %v, want replay-required", errExecute, errExecute)
}
}
func TestMapXAIWebsocketWriteErrorStopsRetryForMessageTooBig(t *testing.T) {
networkWriteErr := errors.New("write: broken pipe")
tests := []struct {
@@ -1246,6 +1290,127 @@ func TestXAIWebsocketsExecuteStreamCompactionTriggerUsesHTTPCompactWithRecordedC
}
}
func TestXAIWebsocketPostCompactionAppendWithoutPreviousReplaysCompactedTranscript(t *testing.T) {
store := &xaiWebsocketIDStateStore{sessions: make(map[string]*xaiWebsocketIDState)}
state := getXAIWebsocketIDState(store, "post-compaction-append-session")
state.replaceTranscriptWithItems([]byte(`{"type":"compaction","encrypted_content":"compact-state"}`))
state.mapDownstreamToUpstream("resp-compact", "")
fullReset := []byte(`{"type":"response.create","model":"grok-4.3","input":[{"type":"message","id":"msg-full"}]}`)
fullMapper := newXAIWebsocketRequestIDMapper(store, "post-compaction-append-session", fullReset)
if full := fullMapper.upstreamRequestPayload(fullReset); len(gjson.GetBytes(full, "input").Array()) != 1 {
t.Fatalf("self-contained response.create unexpectedly replayed compacted transcript: %s", full)
}
payload := []byte(`{"type":"response.append","model":"grok-4.3","input":[{"type":"message","id":"msg-2","role":"user","content":"second"}]}`)
mapper := newXAIWebsocketRequestIDMapper(store, "post-compaction-append-session", payload)
got := mapper.upstreamRequestPayload(payload)
input := gjson.GetBytes(got, "input").Array()
if len(input) != 2 {
t.Fatalf("post-compaction append input len = %d, want 2: %s", len(input), got)
}
if gotType := input[0].Get("type").String(); gotType != "compaction" {
t.Fatalf("post-compaction append input[0].type = %q, want compaction: %s", gotType, got)
}
if gotID := input[1].Get("id").String(); gotID != "msg-2" {
t.Fatalf("post-compaction append input[1].id = %q, want msg-2: %s", gotID, got)
}
state.recordTranscriptTurn(got, []byte(`{"type":"response.completed","response":{"id":"resp-after-compact","output":[{"type":"message","id":"out-2"}]}}`), true)
nextPayload := []byte(`{"type":"response.create","model":"grok-4.3","input":[{"type":"message","id":"msg-3"}]}`)
nextMapper := newXAIWebsocketRequestIDMapper(store, "post-compaction-append-session", nextPayload)
next := nextMapper.upstreamRequestPayload(nextPayload)
if nextInput := gjson.GetBytes(next, "input").Array(); len(nextInput) != 1 || nextInput[0].Get("id").String() != "msg-3" {
t.Fatalf("compacted transcript replay was not cleared after success: %s", next)
}
}
func TestXAIWebsocketPostCompactionWarmupPreservesTranscriptForLaterCompaction(t *testing.T) {
store := &xaiWebsocketIDStateStore{sessions: make(map[string]*xaiWebsocketIDState)}
state := getXAIWebsocketIDState(store, "warmup-reset-session")
state.replaceTranscriptWithItems([]byte(`{"type":"compaction","encrypted_content":"compact-state"}`))
warmupPayload := []byte(`{"type":"response.append","model":"grok-4.3","generate":false,"input":[{"type":"message","id":"warmup-context"}]}`)
warmupMapper := newXAIWebsocketRequestIDMapper(store, "warmup-reset-session", warmupPayload)
warmupUpstream := warmupMapper.upstreamRequestPayload(warmupPayload)
if !warmupMapper.replayedCompactedTranscript {
t.Fatal("post-compaction warmup did not mark full transcript replay")
}
state.recordTranscriptTurn(
warmupUpstream,
[]byte(`{"type":"response.completed","response":{"id":"resp-warmup","output":[]}}`),
true,
)
appendPayload := []byte(`{"type":"response.append","model":"grok-4.3","input":[{"type":"message","id":"msg-after-warmup"}]}`)
appendMapper := newXAIWebsocketRequestIDMapper(store, "warmup-reset-session", appendPayload)
appendUpstream := appendMapper.upstreamRequestPayload(appendPayload)
input := gjson.GetBytes(appendUpstream, "input").Array()
if len(input) != 1 || input[0].Get("id").String() != "msg-after-warmup" {
t.Fatalf("warmup retained pending replay instead of native append: %s", appendUpstream)
}
state.recordTranscriptTurn(
appendUpstream,
[]byte(`{"type":"response.completed","response":{"id":"resp-after-warmup","output":[{"type":"message","id":"out-after-warmup"}]}}`),
false,
)
transcript := gjson.ParseBytes(state.snapshotTranscriptInput()).Array()
wantTypes := []string{"compaction", "message", "message", "message"}
if len(transcript) != len(wantTypes) {
t.Fatalf("post-warmup transcript len = %d, want %d: %s", len(transcript), len(wantTypes), state.snapshotTranscriptInput())
}
for i, wantType := range wantTypes {
if gotType := transcript[i].Get("type").String(); gotType != wantType {
t.Fatalf("post-warmup transcript[%d].type = %q, want %q: %s", i, gotType, wantType, state.snapshotTranscriptInput())
}
}
}
func TestXAIWebsocketEmptyFullResetClearsPendingCompactionReplay(t *testing.T) {
store := &xaiWebsocketIDStateStore{sessions: make(map[string]*xaiWebsocketIDState)}
state := getXAIWebsocketIDState(store, "empty-reset-session")
state.replaceTranscriptWithItems([]byte(`{"type":"compaction","encrypted_content":"stale-compact-state"}`))
state.recordTranscriptTurn(
[]byte(`{"type":"response.create","model":"grok-4.3","input":[]}`),
[]byte(`{"type":"response.completed","response":{"id":"resp-empty","output":[]}}`),
true,
)
appendPayload := []byte(`{"type":"response.append","model":"grok-4.3","input":[{"type":"message","id":"msg-new"}]}`)
mapper := newXAIWebsocketRequestIDMapper(store, "empty-reset-session", appendPayload)
got := mapper.upstreamRequestPayload(appendPayload)
input := gjson.GetBytes(got, "input").Array()
if len(input) != 1 || input[0].Get("id").String() != "msg-new" {
t.Fatalf("empty full reset retained stale compaction replay: %s", got)
}
}
func TestValidateXAIWebsocketCompactionResponse(t *testing.T) {
valid := []byte(`{"id":"resp_compact","output":[{"type":"compaction","encrypted_content":"opaque-state"}]}`)
responseID, item, err := validateXAIWebsocketCompactionResponse(valid)
if err != nil {
t.Fatalf("valid compaction response error: %v", err)
}
if responseID != "resp_compact" || gjson.GetBytes(item, "encrypted_content").String() != "opaque-state" {
t.Fatalf("validated compaction response = id:%q item:%s", responseID, item)
}
for _, payload := range [][]byte{
nil,
[]byte(`{}`),
[]byte(`{"id":"resp_empty","output":[]}`),
[]byte(`{"id":123,"output":[{"type":"compaction","encrypted_content":"opaque"}]}`),
[]byte(`{"id":"resp_object","output":{"0":{"type":"compaction","encrypted_content":"opaque"}}}`),
[]byte(`{"id":"resp_numeric_state","output":[{"type":"compaction","encrypted_content":123}]}`),
[]byte(`{"id":"resp_missing_state","output":[{"type":"compaction"}]}`),
} {
if _, _, errInvalid := validateXAIWebsocketCompactionResponse(payload); errInvalid == nil {
t.Fatalf("invalid compaction response accepted: %s", payload)
}
}
}
func TestBuildXAIWebsocketRequestBodySetsStoreAndKeepsPromptCacheKey(t *testing.T) {
body := []byte(`{"model":"grok-4.3","stream":true,"stream_options":{"include_usage":true},"background":true,"prompt_cache_key":"cache-1","previous_response_id":"resp-prev","instructions":"system prompt","input":[{"type":"message","role":"user","content":"hello"}]}`)