vibe-proxy/backend/sdk/api/handlers/openai/openai_responses_websocket_test.go
2026-08-24 00:10:41 +02:00

5803 lines
232 KiB
Go
Raw Blame History

package openai
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"maps"
"net/http"
"net/http/httptest"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"unicode/utf8"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
requestlogging "github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executionregistry"
coreexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
sdkconfig "github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
"github.com/tidwall/gjson"
)
type homeResponsesWebsocketDispatcher struct {
calls atomic.Int32
}
func (*homeResponsesWebsocketDispatcher) HeartbeatOK() bool { return true }
func (d *homeResponsesWebsocketDispatcher) RPopAuth(context.Context, string, string, http.Header, int) ([]byte, error) {
d.calls.Add(1)
return json.Marshal(coreauth.Auth{
ID: "home-responses-websocket-auth",
Provider: "codex",
Status: coreauth.StatusActive,
Attributes: map[string]string{
"websockets": "true",
},
})
}
func (*homeResponsesWebsocketDispatcher) AbortAmbiguousDispatch() {}
type homeResponsesWebsocketExecutor struct {
calls atomic.Int32
metadata []map[string]any
mu sync.Mutex
}
func (*homeResponsesWebsocketExecutor) Identifier() string { return "codex" }
func (*homeResponsesWebsocketExecutor) Execute(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{}, errors.New("not implemented")
}
func (e *homeResponsesWebsocketExecutor) ExecuteStream(_ context.Context, _ *coreauth.Auth, _ coreexecutor.Request, opts coreexecutor.Options) (*coreexecutor.StreamResult, error) {
e.calls.Add(1)
e.mu.Lock()
e.metadata = append(e.metadata, maps.Clone(opts.Metadata))
e.mu.Unlock()
if lifecycle, ok := opts.ExecutionLifecycle.(interface{ Retain() }); ok {
lifecycle.Retain()
}
chunks := make(chan coreexecutor.StreamChunk, 1)
chunks <- coreexecutor.StreamChunk{Payload: []byte(`{"type":"response.completed","response":{"id":"home-response","output":[]}}`)}
close(chunks)
return &coreexecutor.StreamResult{Chunks: chunks}, nil
}
func (*homeResponsesWebsocketExecutor) Refresh(context.Context, *coreauth.Auth) (*coreauth.Auth, error) {
return nil, errors.New("not implemented")
}
func (*homeResponsesWebsocketExecutor) CountTokens(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{}, errors.New("not implemented")
}
func (*homeResponsesWebsocketExecutor) HttpRequest(context.Context, *coreauth.Auth, *http.Request) (*http.Response, error) {
return nil, errors.New("not implemented")
}
func TestResponsesWebsocketHomeSelectedAuthCallbackPinsAndReusesFirstSelection(t *testing.T) {
gin.SetMode(gin.TestMode)
dispatcher := &homeResponsesWebsocketDispatcher{}
executor := &homeResponsesWebsocketExecutor{}
manager := coreauth.NewManager(nil, nil, nil)
manager.SetConfig(&internalconfig.Config{Home: internalconfig.HomeConfig{Enabled: true}})
manager.PublishHomeDispatch(dispatcher, executionregistry.New(), 1)
manager.RegisterExecutor(executor)
registry.GetGlobalRegistry().RegisterClient("home-responses-websocket-auth", "codex", []*registry.ModelInfo{{ID: "gpt-5.4"}})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, errDial := websocket.DefaultDialer.Dial(wsURL, nil)
if errDial != nil {
t.Fatalf("dial websocket: %v", errDial)
}
defer func() {
if errClose := conn.Close(); errClose != nil {
t.Errorf("close websocket: %v", errClose)
}
}()
requests := []string{
`{"type":"response.create","model":"gpt-5.4","input":[]}`,
`{"type":"response.create","model":"gpt-5.4","input":[]}`,
}
for index, request := range requests {
if errWrite := conn.WriteMessage(websocket.TextMessage, []byte(request)); errWrite != nil {
t.Fatalf("write websocket request %d: %v", index+1, errWrite)
}
_, payload, errRead := conn.ReadMessage()
if errRead != nil {
t.Fatalf("read websocket response %d: %v", index+1, errRead)
}
if got := gjson.GetBytes(payload, "type").String(); got != wsEventTypeCompleted {
t.Fatalf("response %d type = %q, want %q: %s", index+1, got, wsEventTypeCompleted, payload)
}
if index == 0 {
executor.mu.Lock()
firstMetadata := maps.Clone(executor.metadata[0])
executor.mu.Unlock()
sessionID, _ := firstMetadata[coreexecutor.ExecutionSessionMetadataKey].(string)
if _, ok := manager.GetExecutionSessionAuthByID(sessionID, "home-responses-websocket-auth"); !ok {
t.Fatal("first selected-auth callback did not stage the session runtime auth")
}
}
}
executor.mu.Lock()
metadata := append([]map[string]any(nil), executor.metadata...)
executor.mu.Unlock()
if len(metadata) != 2 {
t.Fatalf("executor metadata calls = %d, want 2", len(metadata))
}
if got := metadata[1][coreexecutor.PinnedAuthMetadataKey]; got != "home-responses-websocket-auth" {
t.Fatalf("second turn pinned auth metadata = %#v, want home selected auth (first metadata: %#v, second metadata: %#v)", got, metadata[0], metadata[1])
}
if got := dispatcher.calls.Load(); got != 1 {
t.Fatalf("Home RPOP calls = %d, want 1 after selected-auth callback pin", got)
}
if got := executor.calls.Load(); got != 2 {
t.Fatalf("executor calls = %d, want 2", got)
}
}
func TestWebsocketReplayCloseRequiresTypedSignal(t *testing.T) {
matched, payload := websocketClosePayloadForUpstreamError(responsesWebsocketHTTPReplayRequiredError())
if !matched || len(payload) == 0 {
t.Fatalf("typed replay signal matched=%t payload_len=%d, want close payload", matched, len(payload))
}
spoofed := websocketPinnedFailoverStatusError{
status: http.StatusUpgradeRequired,
msg: `{"error":{"code":"upstream_http_replay_required"}}`,
}
if matched, _ := websocketClosePayloadForUpstreamError(spoofed); matched {
t.Fatal("untyped upstream error spoofed replay close")
}
}
func TestResponsesWebsocketRequestRequiresCurrentUpstream(t *testing.T) {
cases := []struct {
name string
payload string
want bool
}{
{name: "incremental create", payload: `{"type":"response.create","previous_response_id":"resp-1","input":[]}`, want: true},
{name: "append", payload: `{"type":"response.append","input":[]}`, want: true},
{name: "full create", payload: `{"type":"response.create","input":[]}`, want: false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := responsesWebsocketRequestRequiresCurrentUpstream([]byte(tc.payload)); got != tc.want {
t.Fatalf("responsesWebsocketRequestRequiresCurrentUpstream() = %t, want %t", got, tc.want)
}
})
}
}
func TestResponsesWebsocketNativePassthroughRequiresImmediatelyPreviousAuth(t *testing.T) {
if !responsesWebsocketNativePassthroughAllowed(responsesWebsocketUpstreamModeWS, true, "auth-a", "auth-a") {
t.Fatal("matching immediate websocket auth did not allow native passthrough")
}
if responsesWebsocketNativePassthroughAllowed(responsesWebsocketUpstreamModeWS, true, "auth-a", "auth-b") {
t.Fatal("restored auth from an older provider session allowed native passthrough")
}
if responsesWebsocketNativePassthroughAllowed(responsesWebsocketUpstreamModeHTTP, true, "auth-a", "auth-a") {
t.Fatal("HTTP mode allowed native websocket passthrough")
}
}
func TestWriteWebsocketCloseForUpstreamErrorMirrorsMessageTooBig(t *testing.T) {
tests := []struct {
name string
err error
reason string
}{
{
name: "raw close error",
err: &websocket.CloseError{
Code: websocket.CloseMessageTooBig,
Text: "message too big",
},
reason: "message too big",
},
{
name: "mapped stream error",
err: websocketPinnedFailoverStatusError{
status: http.StatusRequestEntityTooLarge,
msg: `{"error":{"message":"upstream websocket message too big","code":"message_too_big"}}`,
},
reason: "upstream websocket message too big",
},
{
name: "multibyte reason stays valid",
err: &websocket.CloseError{
Code: websocket.CloseMessageTooBig,
Text: strings.Repeat("🙂", 31),
},
reason: strings.Repeat("🙂", 30),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
serverErr := make(chan error, 1)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := responsesWebsocketUpgrader.Upgrade(w, r, nil)
if err != nil {
serverErr <- err
return
}
matched, errWrite := writeWebsocketCloseForUpstreamError(conn, tt.err)
if !matched && errWrite == nil {
errWrite = errors.New("message-too-big error did not match")
}
if errClose := conn.Close(); errWrite == nil {
errWrite = errClose
}
serverErr <- errWrite
}))
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
if err = conn.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
t.Fatalf("set read deadline: %v", err)
}
_, _, err = conn.ReadMessage()
var closeErr *websocket.CloseError
if !errors.As(err, &closeErr) {
t.Fatalf("expected websocket close error, got %v", err)
}
if closeErr.Code != websocket.CloseMessageTooBig {
t.Fatalf("expected close code 1009, got %d", closeErr.Code)
}
if closeErr.Text != tt.reason {
t.Fatalf("expected close reason %q, got %q", tt.reason, closeErr.Text)
}
if err = <-serverErr; err != nil {
t.Fatalf("close server websocket: %v", err)
}
})
}
}
func TestResponsesWebsocketWriterCloseDoesNotWaitForActiveDataWriter(t *testing.T) {
serverErrCh := make(chan error, 1)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := responsesWebsocketUpgrader.Upgrade(w, r, nil)
if err != nil {
serverErrCh <- err
return
}
writer := newResponsesWebsocketWriter(conn)
// Holding writeMu models a data writer blocked inside WriteMessage. The
// upstream-close path must hard-close the socket instead of waiting for it.
writer.writeMu.Lock()
closeDone := make(chan error, 1)
go func() {
matched, errClose := writer.closeForUpstreamError(&websocket.CloseError{
Code: websocket.CloseMessageTooBig,
Text: "message too big",
})
if !matched && errClose == nil {
errClose = errors.New("message-too-big error did not match")
}
closeDone <- errClose
}()
select {
case errClose := <-closeDone:
writer.writeMu.Unlock()
serverErrCh <- errClose
case <-time.After(time.Second):
writer.writeMu.Unlock()
serverErrCh <- errors.New("close waited behind active data writer")
}
}))
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
if err = conn.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
t.Fatalf("set read deadline: %v", err)
}
if _, _, err = conn.ReadMessage(); err == nil {
t.Fatal("client read succeeded, want connection closure")
}
if errServer := <-serverErrCh; errServer != nil {
t.Fatalf("server error: %v", errServer)
}
}
func TestResponsesWebsocketGenericDisconnectDoesNotWaitForActiveDataWriter(t *testing.T) {
serverErrCh := make(chan error, 1)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := responsesWebsocketUpgrader.Upgrade(w, r, nil)
if err != nil {
serverErrCh <- err
return
}
writer := newResponsesWebsocketWriter(conn)
writer.writeMu.Lock()
closeDone := make(chan struct{})
go func() {
writer.closeForUpstreamDisconnect(&websocket.CloseError{
Code: websocket.CloseAbnormalClosure,
Text: "unexpected EOF",
})
close(closeDone)
}()
select {
case <-closeDone:
writer.writeMu.Unlock()
serverErrCh <- nil
case <-time.After(time.Second):
writer.writeMu.Unlock()
serverErrCh <- errors.New("generic disconnect waited behind active data writer")
}
}))
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
if err = conn.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
t.Fatalf("set read deadline: %v", err)
}
if _, _, err = conn.ReadMessage(); err == nil {
t.Fatal("client read succeeded, want connection closure")
}
if errServer := <-serverErrCh; errServer != nil {
t.Fatalf("server error: %v", errServer)
}
}
func TestTruncateWebsocketCloseReason(t *testing.T) {
tests := []struct {
name string
reason string
maxBytes int
want string
}{
{
name: "non-positive limit",
reason: "message too big",
maxBytes: 0,
want: "",
},
{
name: "short valid reason unchanged",
reason: "message too big",
maxBytes: wsCloseReasonMaxBytes,
want: "message too big",
},
{
name: "long ascii reason",
reason: strings.Repeat("x", 1<<20),
maxBytes: wsCloseReasonMaxBytes,
want: strings.Repeat("x", wsCloseReasonMaxBytes),
},
{
name: "long invalid reason",
reason: strings.Repeat("\xff", 1<<20),
maxBytes: wsCloseReasonMaxBytes,
want: strings.Repeat("<22>", wsCloseReasonMaxBytes/utf8.RuneLen(utf8.RuneError)),
},
{
name: "multibyte rune does not fit",
reason: "ab🙂cd",
maxBytes: 5,
want: "ab",
},
{
name: "invalid bytes become replacement runes",
reason: string([]byte{'a', 0xff, 0xfe, 'b'}),
maxBytes: 8,
want: "a<><61>b",
},
{
name: "invalid replacement does not cross limit",
reason: string([]byte{'a', 'b', 0xff, 'c'}),
maxBytes: 4,
want: "ab",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := truncateWebsocketCloseReason(tt.reason, tt.maxBytes)
if got != tt.want {
t.Fatalf("truncateWebsocketCloseReason() = %q, want %q", got, tt.want)
}
if !utf8.ValidString(got) {
t.Fatalf("truncateWebsocketCloseReason() returned invalid UTF-8: %q", got)
}
if tt.maxBytes > 0 && len(got) > tt.maxBytes {
t.Fatalf("truncateWebsocketCloseReason() returned %d bytes, limit %d", len(got), tt.maxBytes)
}
})
}
}
func TestForwardResponsesWebsocketMirrorsMappedMessageTooBig(t *testing.T) {
gin.SetMode(gin.TestMode)
serverErrCh := make(chan error, 1)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := responsesWebsocketUpgrader.Upgrade(w, r, nil)
if err != nil {
serverErrCh <- err
return
}
defer func() { _ = conn.Close() }()
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
ctx.Request = r
data := make(chan []byte)
errCh := make(chan *interfaces.ErrorMessage, 1)
errCh <- &interfaces.ErrorMessage{
StatusCode: http.StatusRequestEntityTooLarge,
Error: websocketPinnedFailoverStatusError{
status: http.StatusRequestEntityTooLarge,
msg: `{"error":{"message":"upstream websocket message too big","code":"message_too_big"}}`,
},
}
h := NewOpenAIResponsesAPIHandler(handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, nil))
_, _, _, errMsg, errForward := h.forwardResponsesWebsocket(
ctx,
newResponsesWebsocketWriter(conn),
func(...interface{}) {},
data,
errCh,
newInMemoryWebsocketTimelineLog(),
"session-1",
)
if errMsg == nil || errMsg.StatusCode != http.StatusRequestEntityTooLarge {
serverErrCh <- fmt.Errorf("forward error message = %#v, want status %d", errMsg, http.StatusRequestEntityTooLarge)
return
}
if !errors.Is(errForward, websocket.ErrCloseSent) {
serverErrCh <- fmt.Errorf("forward error = %v, want %v", errForward, websocket.ErrCloseSent)
return
}
serverErrCh <- nil
}))
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
if err = conn.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
t.Fatalf("set read deadline: %v", err)
}
_, _, err = conn.ReadMessage()
var closeErr *websocket.CloseError
if !errors.As(err, &closeErr) {
t.Fatalf("expected websocket close error, got %v", err)
}
if closeErr.Code != websocket.CloseMessageTooBig {
t.Fatalf("close code = %d, want %d", closeErr.Code, websocket.CloseMessageTooBig)
}
if err = <-serverErrCh; err != nil {
t.Fatalf("server error: %v", err)
}
}
func TestForwardResponsesWebsocketMirrorsPayloadMessageTooBig(t *testing.T) {
gin.SetMode(gin.TestMode)
serverErrCh := make(chan error, 1)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := responsesWebsocketUpgrader.Upgrade(w, r, nil)
if err != nil {
serverErrCh <- err
return
}
defer func() { _ = conn.Close() }()
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
ctx.Request = r
data := make(chan []byte, 1)
errCh := make(chan *interfaces.ErrorMessage)
data <- []byte(`{"type":"error","status":413,"error":{"message":"upstream websocket message too big","code":"message_too_big"}}`)
close(data)
close(errCh)
h := NewOpenAIResponsesAPIHandler(handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, nil))
_, _, _, errMsg, errForward := h.forwardResponsesWebsocket(
ctx,
newResponsesWebsocketWriter(conn),
func(...interface{}) {},
data,
errCh,
newInMemoryWebsocketTimelineLog(),
"session-1",
)
if errMsg == nil || errMsg.StatusCode != http.StatusRequestEntityTooLarge {
serverErrCh <- fmt.Errorf("forward error message = %#v, want status %d", errMsg, http.StatusRequestEntityTooLarge)
return
}
if !errors.Is(errForward, websocket.ErrCloseSent) {
serverErrCh <- fmt.Errorf("forward error = %v, want %v", errForward, websocket.ErrCloseSent)
return
}
serverErrCh <- nil
}))
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
if err = conn.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
t.Fatalf("set read deadline: %v", err)
}
_, _, err = conn.ReadMessage()
var closeErr *websocket.CloseError
if !errors.As(err, &closeErr) {
t.Fatalf("expected websocket close error, got %v", err)
}
if closeErr.Code != websocket.CloseMessageTooBig {
t.Fatalf("close code = %d, want %d", closeErr.Code, websocket.CloseMessageTooBig)
}
if err = <-serverErrCh; err != nil {
t.Fatalf("server error: %v", err)
}
}
type websocketCaptureExecutor struct {
streamCalls int
payloads [][]byte
}
type websocketProviderCaptureExecutor struct {
provider string
websocketCaptureExecutor
}
type websocketProviderRouteHost struct{}
func (*websocketProviderRouteHost) HasModelRouters() bool { return true }
func (*websocketProviderRouteHost) RouteModel(_ context.Context, req pluginapi.ModelRouteRequest) (pluginapi.ModelRouteResponse, bool) {
if !gjson.GetBytes(req.Body, "route_to_claude").Bool() {
return pluginapi.ModelRouteResponse{}, false
}
return pluginapi.ModelRouteResponse{
Handled: true,
TargetKind: pluginapi.ModelRouteTargetProvider,
Target: "claude",
TargetModel: "claude-provider-route-target",
}, true
}
type websocketCompactionCaptureExecutor struct {
mu sync.Mutex
streamPayloads [][]byte
compactPayload []byte
}
type orderedWebsocketSelector struct {
mu sync.Mutex
order []string
cursor int
}
func (s *orderedWebsocketSelector) Pick(_ context.Context, _ string, _ string, _ coreexecutor.Options, auths []*coreauth.Auth) (*coreauth.Auth, error) {
s.mu.Lock()
defer s.mu.Unlock()
if len(auths) == 0 {
return nil, errors.New("no auth available")
}
for len(s.order) > 0 && s.cursor < len(s.order) {
authID := strings.TrimSpace(s.order[s.cursor])
s.cursor++
for _, auth := range auths {
if auth != nil && auth.ID == authID {
return auth, nil
}
}
}
for _, auth := range auths {
if auth != nil {
return auth, nil
}
}
return nil, errors.New("no auth available")
}
type websocketAuthCaptureExecutor struct {
mu sync.Mutex
authIDs []string
}
type websocketPinnedFailoverExecutor struct {
mu sync.Mutex
failStatus int
authIDs []string
calls map[string]int
payloads map[string][][]byte
}
type websocketBootstrapFallbackExecutor struct {
mu sync.Mutex
authIDs []string
payloads map[string][][]byte
}
type websocketDirectCaptureExecutor struct {
mu sync.Mutex
provider string
failStatus int
authIDs []string
models []string
payloads [][]byte
requiredUpstreamWebsocket []bool
done chan struct{}
doneOnce sync.Once
}
type websocketCanonicalRollbackExecutor struct {
mu sync.Mutex
payloads [][]byte
calls int
// failErr overrides the default second-call failure when set.
failErr error
}
type websocketPinnedFailoverStatusError struct {
status int
msg string
}
func (e websocketPinnedFailoverStatusError) Error() string { return e.msg }
func (e websocketPinnedFailoverStatusError) StatusCode() int { return e.status }
func (e *websocketBootstrapFallbackExecutor) Identifier() string { return "test-provider" }
func (e *websocketBootstrapFallbackExecutor) Execute(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{}, errors.New("not implemented")
}
func (e *websocketBootstrapFallbackExecutor) ExecuteStream(_ context.Context, auth *coreauth.Auth, req coreexecutor.Request, _ coreexecutor.Options) (*coreexecutor.StreamResult, error) {
authID := ""
if auth != nil {
authID = auth.ID
}
e.mu.Lock()
if e.payloads == nil {
e.payloads = make(map[string][][]byte)
}
e.authIDs = append(e.authIDs, authID)
e.payloads[authID] = append(e.payloads[authID], bytes.Clone(req.Payload))
e.mu.Unlock()
chunks := make(chan coreexecutor.StreamChunk, 1)
if authID == "auth-ws" {
chunks <- coreexecutor.StreamChunk{Err: websocketPinnedFailoverStatusError{
status: http.StatusUpgradeRequired,
msg: `{"error":{"message":"websocket bootstrap failed","type":"server_error","code":"ws_failed"}}`,
}}
close(chunks)
return &coreexecutor.StreamResult{Chunks: chunks}, nil
}
chunks <- coreexecutor.StreamChunk{Payload: []byte(`{"type":"response.completed","response":{"id":"resp-http","output":[{"type":"message","id":"out-http"}]}}`)}
close(chunks)
return &coreexecutor.StreamResult{Chunks: chunks}, nil
}
func (e *websocketBootstrapFallbackExecutor) Refresh(_ context.Context, auth *coreauth.Auth) (*coreauth.Auth, error) {
return auth, nil
}
func (e *websocketBootstrapFallbackExecutor) CountTokens(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{}, errors.New("not implemented")
}
func (e *websocketBootstrapFallbackExecutor) HttpRequest(context.Context, *coreauth.Auth, *http.Request) (*http.Response, error) {
return nil, errors.New("not implemented")
}
func (e *websocketBootstrapFallbackExecutor) AuthIDs() []string {
e.mu.Lock()
defer e.mu.Unlock()
return append([]string(nil), e.authIDs...)
}
func (e *websocketBootstrapFallbackExecutor) Payloads(authID string) [][]byte {
e.mu.Lock()
defer e.mu.Unlock()
src := e.payloads[authID]
out := make([][]byte, len(src))
for i := range src {
out[i] = bytes.Clone(src[i])
}
return out
}
func (e *websocketDirectCaptureExecutor) Identifier() string {
if e != nil && strings.TrimSpace(e.provider) != "" {
return strings.TrimSpace(e.provider)
}
return "codex"
}
func (e *websocketDirectCaptureExecutor) Execute(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{}, errors.New("not implemented")
}
func (e *websocketDirectCaptureExecutor) ExecuteStream(ctx context.Context, auth *coreauth.Auth, req coreexecutor.Request, _ coreexecutor.Options) (*coreexecutor.StreamResult, error) {
authID := ""
if auth != nil {
authID = auth.ID
}
e.mu.Lock()
e.authIDs = append(e.authIDs, authID)
e.models = append(e.models, req.Model)
e.payloads = append(e.payloads, bytes.Clone(req.Payload))
e.requiredUpstreamWebsocket = append(e.requiredUpstreamWebsocket, coreexecutor.RequiredUpstreamWebsocket(ctx))
count := len(e.payloads)
failStatus := e.failStatus
e.mu.Unlock()
chunks := make(chan coreexecutor.StreamChunk, 1)
if failStatus > 0 {
chunks <- coreexecutor.StreamChunk{Err: websocketPinnedFailoverStatusError{
status: failStatus,
msg: `{"error":{"message":"routed provider failed","type":"authentication_error","code":"invalid_api_key"}}`,
}}
close(chunks)
return &coreexecutor.StreamResult{Chunks: chunks}, nil
}
responseID := fmt.Sprintf("resp-%d", count)
chunks <- coreexecutor.StreamChunk{Payload: []byte(fmt.Sprintf(`{"type":"response.completed","response":{"id":%q,"output":[{"type":"message","id":"out-%d"}]}}`, responseID, count))}
close(chunks)
if count >= 2 && e.done != nil {
e.doneOnce.Do(func() {
close(e.done)
})
}
return &coreexecutor.StreamResult{Chunks: chunks}, nil
}
func (e *websocketDirectCaptureExecutor) Refresh(_ context.Context, auth *coreauth.Auth) (*coreauth.Auth, error) {
return auth, nil
}
func (e *websocketDirectCaptureExecutor) CountTokens(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{}, errors.New("not implemented")
}
func (e *websocketDirectCaptureExecutor) HttpRequest(context.Context, *coreauth.Auth, *http.Request) (*http.Response, error) {
return nil, errors.New("not implemented")
}
func (e *websocketDirectCaptureExecutor) Payloads() [][]byte {
e.mu.Lock()
defer e.mu.Unlock()
out := make([][]byte, len(e.payloads))
for i := range e.payloads {
out[i] = bytes.Clone(e.payloads[i])
}
return out
}
func (e *websocketDirectCaptureExecutor) AuthIDs() []string {
e.mu.Lock()
defer e.mu.Unlock()
return append([]string(nil), e.authIDs...)
}
func (e *websocketDirectCaptureExecutor) Models() []string {
e.mu.Lock()
defer e.mu.Unlock()
return append([]string(nil), e.models...)
}
func (e *websocketDirectCaptureExecutor) RequiredUpstreamWebsocketFlags() []bool {
e.mu.Lock()
defer e.mu.Unlock()
return append([]bool(nil), e.requiredUpstreamWebsocket...)
}
func (e *websocketCanonicalRollbackExecutor) Identifier() string { return "xai" }
func (e *websocketCanonicalRollbackExecutor) Execute(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{}, errors.New("not implemented")
}
func (e *websocketCanonicalRollbackExecutor) ExecuteStream(_ context.Context, _ *coreauth.Auth, req coreexecutor.Request, _ coreexecutor.Options) (*coreexecutor.StreamResult, error) {
e.mu.Lock()
e.calls++
call := e.calls
e.payloads = append(e.payloads, bytes.Clone(req.Payload))
failErr := e.failErr
e.mu.Unlock()
chunks := make(chan coreexecutor.StreamChunk, 1)
if call == 2 {
if failErr == nil {
failErr = websocketPinnedFailoverStatusError{
status: http.StatusBadRequest,
msg: `{"error":{"message":"bad turn","type":"invalid_request_error","code":"invalid_request"}}`,
}
}
chunks <- coreexecutor.StreamChunk{Err: failErr}
close(chunks)
return &coreexecutor.StreamResult{Chunks: chunks}, nil
}
chunks <- coreexecutor.StreamChunk{Payload: []byte(fmt.Sprintf(`{"type":"response.completed","response":{"id":"resp-%d","output":[{"type":"message","id":"out-%d"}]}}`, call, call))}
close(chunks)
return &coreexecutor.StreamResult{Chunks: chunks}, nil
}
func (e *websocketCanonicalRollbackExecutor) Refresh(_ context.Context, auth *coreauth.Auth) (*coreauth.Auth, error) {
return auth, nil
}
func (e *websocketCanonicalRollbackExecutor) CountTokens(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{}, errors.New("not implemented")
}
func (e *websocketCanonicalRollbackExecutor) HttpRequest(context.Context, *coreauth.Auth, *http.Request) (*http.Response, error) {
return nil, errors.New("not implemented")
}
func (e *websocketCanonicalRollbackExecutor) Payloads() [][]byte {
e.mu.Lock()
defer e.mu.Unlock()
out := make([][]byte, len(e.payloads))
for i := range e.payloads {
out[i] = bytes.Clone(e.payloads[i])
}
return out
}
type websocketUpstreamDisconnectExecutor struct {
mu sync.Mutex
provider string
subscribed chan string
sessions map[string]chan error
}
func (e *websocketUpstreamDisconnectExecutor) Identifier() string {
if provider := strings.TrimSpace(e.provider); provider != "" {
return provider
}
return "codex"
}
func (e *websocketUpstreamDisconnectExecutor) UpstreamDisconnectChan(sessionID string) <-chan error {
sessionID = strings.TrimSpace(sessionID)
if sessionID == "" {
return nil
}
e.mu.Lock()
if e.sessions == nil {
e.sessions = make(map[string]chan error)
}
ch, ok := e.sessions[sessionID]
if !ok {
ch = make(chan error, 1)
e.sessions[sessionID] = ch
}
subscribed := e.subscribed
e.mu.Unlock()
if subscribed != nil {
select {
case subscribed <- sessionID:
default:
}
}
return ch
}
func (e *websocketUpstreamDisconnectExecutor) TriggerDisconnect(sessionID string, err error) {
sessionID = strings.TrimSpace(sessionID)
if sessionID == "" {
return
}
e.mu.Lock()
ch := e.sessions[sessionID]
delete(e.sessions, sessionID)
e.mu.Unlock()
if ch == nil {
return
}
select {
case ch <- err:
default:
}
close(ch)
}
func (e *websocketUpstreamDisconnectExecutor) Execute(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{}, errors.New("not implemented")
}
func (e *websocketUpstreamDisconnectExecutor) ExecuteStream(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (*coreexecutor.StreamResult, error) {
return nil, errors.New("not implemented")
}
func (e *websocketUpstreamDisconnectExecutor) Refresh(_ context.Context, auth *coreauth.Auth) (*coreauth.Auth, error) {
return auth, nil
}
func (e *websocketUpstreamDisconnectExecutor) CountTokens(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{}, errors.New("not implemented")
}
func (e *websocketUpstreamDisconnectExecutor) HttpRequest(context.Context, *coreauth.Auth, *http.Request) (*http.Response, error) {
return nil, errors.New("not implemented")
}
func (e *websocketAuthCaptureExecutor) Identifier() string { return "test-provider" }
func (e *websocketAuthCaptureExecutor) Execute(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{}, errors.New("not implemented")
}
func (e *websocketAuthCaptureExecutor) ExecuteStream(_ context.Context, auth *coreauth.Auth, _ coreexecutor.Request, _ coreexecutor.Options) (*coreexecutor.StreamResult, error) {
e.mu.Lock()
if auth != nil {
e.authIDs = append(e.authIDs, auth.ID)
}
e.mu.Unlock()
chunks := make(chan coreexecutor.StreamChunk, 1)
chunks <- coreexecutor.StreamChunk{Payload: []byte(`{"type":"response.completed","response":{"id":"resp-upstream","output":[{"type":"message","id":"out-1"}]}}`)}
close(chunks)
return &coreexecutor.StreamResult{Chunks: chunks}, nil
}
func (e *websocketAuthCaptureExecutor) Refresh(_ context.Context, auth *coreauth.Auth) (*coreauth.Auth, error) {
return auth, nil
}
func (e *websocketAuthCaptureExecutor) CountTokens(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{}, errors.New("not implemented")
}
func (e *websocketAuthCaptureExecutor) HttpRequest(context.Context, *coreauth.Auth, *http.Request) (*http.Response, error) {
return nil, errors.New("not implemented")
}
func (e *websocketAuthCaptureExecutor) AuthIDs() []string {
e.mu.Lock()
defer e.mu.Unlock()
return append([]string(nil), e.authIDs...)
}
func (e *websocketPinnedFailoverExecutor) Identifier() string { return "xai" }
func (e *websocketPinnedFailoverExecutor) Execute(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{}, errors.New("not implemented")
}
func (e *websocketPinnedFailoverExecutor) ExecuteStream(_ context.Context, auth *coreauth.Auth, req coreexecutor.Request, _ coreexecutor.Options) (*coreexecutor.StreamResult, error) {
authID := ""
if auth != nil {
authID = auth.ID
}
e.mu.Lock()
if e.calls == nil {
e.calls = make(map[string]int)
}
if e.payloads == nil {
e.payloads = make(map[string][][]byte)
}
e.authIDs = append(e.authIDs, authID)
e.calls[authID]++
call := e.calls[authID]
e.payloads[authID] = append(e.payloads[authID], bytes.Clone(req.Payload))
e.mu.Unlock()
if authID == "auth-a" && call == 2 {
chunks := make(chan coreexecutor.StreamChunk, 1)
chunks <- coreexecutor.StreamChunk{Err: websocketPinnedFailoverStatusError{
status: e.failStatus,
msg: fmt.Sprintf(`{"error":{"message":"credential failed","status":%d}}`, e.failStatus),
}}
close(chunks)
return &coreexecutor.StreamResult{Chunks: chunks}, nil
}
chunks := make(chan coreexecutor.StreamChunk, 1)
chunks <- coreexecutor.StreamChunk{Payload: []byte(fmt.Sprintf(`{"type":"response.completed","response":{"id":"resp-%s-%d","output":[{"type":"message","id":"out-%s-%d"}]}}`, authID, call, authID, call))}
close(chunks)
return &coreexecutor.StreamResult{Chunks: chunks}, nil
}
func (e *websocketPinnedFailoverExecutor) Refresh(_ context.Context, auth *coreauth.Auth) (*coreauth.Auth, error) {
return auth, nil
}
func (e *websocketPinnedFailoverExecutor) CountTokens(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{}, errors.New("not implemented")
}
func (e *websocketPinnedFailoverExecutor) HttpRequest(context.Context, *coreauth.Auth, *http.Request) (*http.Response, error) {
return nil, errors.New("not implemented")
}
func (e *websocketPinnedFailoverExecutor) AuthIDs() []string {
e.mu.Lock()
defer e.mu.Unlock()
return append([]string(nil), e.authIDs...)
}
func (e *websocketPinnedFailoverExecutor) Payloads(authID string) [][]byte {
e.mu.Lock()
defer e.mu.Unlock()
src := e.payloads[authID]
out := make([][]byte, len(src))
for i := range src {
out[i] = bytes.Clone(src[i])
}
return out
}
func (e *websocketCaptureExecutor) Identifier() string { return "test-provider" }
func (e *websocketProviderCaptureExecutor) Identifier() string {
if e != nil && strings.TrimSpace(e.provider) != "" {
return strings.TrimSpace(e.provider)
}
return "test-provider"
}
func (e *websocketCaptureExecutor) Execute(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{}, errors.New("not implemented")
}
func (e *websocketCaptureExecutor) ExecuteStream(_ context.Context, _ *coreauth.Auth, req coreexecutor.Request, _ coreexecutor.Options) (*coreexecutor.StreamResult, error) {
e.streamCalls++
e.payloads = append(e.payloads, bytes.Clone(req.Payload))
chunks := make(chan coreexecutor.StreamChunk, 1)
chunks <- coreexecutor.StreamChunk{Payload: []byte(`{"type":"response.completed","response":{"id":"resp-upstream","output":[{"type":"message","id":"out-1"}]}}`)}
close(chunks)
return &coreexecutor.StreamResult{Chunks: chunks}, nil
}
func (e *websocketCaptureExecutor) Refresh(_ context.Context, auth *coreauth.Auth) (*coreauth.Auth, error) {
return auth, nil
}
func (e *websocketCaptureExecutor) CountTokens(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{}, errors.New("not implemented")
}
func (e *websocketCaptureExecutor) HttpRequest(context.Context, *coreauth.Auth, *http.Request) (*http.Response, error) {
return nil, errors.New("not implemented")
}
func (e *websocketCompactionCaptureExecutor) Identifier() string { return "test-provider" }
func (e *websocketCompactionCaptureExecutor) Execute(_ context.Context, _ *coreauth.Auth, req coreexecutor.Request, opts coreexecutor.Options) (coreexecutor.Response, error) {
e.mu.Lock()
e.compactPayload = bytes.Clone(req.Payload)
e.mu.Unlock()
if opts.Alt != "responses/compact" {
return coreexecutor.Response{}, fmt.Errorf("unexpected non-compact execute alt: %q", opts.Alt)
}
return coreexecutor.Response{Payload: []byte(`{"id":"cmp-1","object":"response.compaction"}`)}, nil
}
func (e *websocketCompactionCaptureExecutor) ExecuteStream(_ context.Context, _ *coreauth.Auth, req coreexecutor.Request, _ coreexecutor.Options) (*coreexecutor.StreamResult, error) {
e.mu.Lock()
callIndex := len(e.streamPayloads)
e.streamPayloads = append(e.streamPayloads, bytes.Clone(req.Payload))
e.mu.Unlock()
var payload []byte
switch callIndex {
case 0:
payload = []byte(`{"type":"response.completed","response":{"id":"resp-1","output":[{"type":"function_call","id":"fc-1","call_id":"call-1","name":"tool"}]}}`)
case 1:
payload = []byte(`{"type":"response.completed","response":{"id":"resp-2","output":[{"type":"message","id":"assistant-1"}]}}`)
default:
payload = []byte(`{"type":"response.completed","response":{"id":"resp-3","output":[{"type":"message","id":"assistant-2"}]}}`)
}
chunks := make(chan coreexecutor.StreamChunk, 1)
chunks <- coreexecutor.StreamChunk{Payload: payload}
close(chunks)
return &coreexecutor.StreamResult{Chunks: chunks}, nil
}
func (e *websocketCompactionCaptureExecutor) Refresh(_ context.Context, auth *coreauth.Auth) (*coreauth.Auth, error) {
return auth, nil
}
func (e *websocketCompactionCaptureExecutor) CountTokens(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{}, errors.New("not implemented")
}
func (e *websocketCompactionCaptureExecutor) HttpRequest(context.Context, *coreauth.Auth, *http.Request) (*http.Response, error) {
return nil, errors.New("not implemented")
}
func TestNormalizeResponsesWebsocketRequestCreate(t *testing.T) {
raw := []byte(`{"type":"response.create","model":"test-model","stream":false,"input":[{"type":"message","id":"msg-1"}]}`)
normalized, last, errMsg := normalizeResponsesWebsocketRequest(raw, nil, nil)
if errMsg != nil {
t.Fatalf("unexpected error: %v", errMsg.Error)
}
if gjson.GetBytes(normalized, "type").Exists() {
t.Fatalf("normalized create request must not include type field")
}
if !gjson.GetBytes(normalized, "stream").Bool() {
t.Fatalf("normalized create request must force stream=true")
}
if gjson.GetBytes(normalized, "model").String() != "test-model" {
t.Fatalf("unexpected model: %s", gjson.GetBytes(normalized, "model").String())
}
if !bytes.Equal(last, normalized) {
t.Fatalf("last request snapshot should match normalized request")
}
}
func TestNormalizeResponseSubsequentRequestBoundsTranscriptAllocations(t *testing.T) {
if raceDetectorEnabled {
t.Skip("allocation budgets are not meaningful with race detector instrumentation")
}
makeInput := func(count int, role string) string {
var input strings.Builder
input.WriteByte('[')
content := strings.Repeat("x", 1024)
for index := 0; index < count; index++ {
if index > 0 {
input.WriteByte(',')
}
fmt.Fprintf(&input, `{"type":"message","role":%q,"id":"%s-%d","content":%q}`, role, role, index, content)
}
input.WriteByte(']')
return input.String()
}
lastRequest := []byte(`{"model":"test-model","stream":true,"input":` + makeInput(128, "user") + `}`)
lastResponseOutput := []byte(makeInput(64, "assistant"))
raw := []byte(`{"type":"response.create","input":[{"type":"message","role":"user","id":"user-next","content":"continue"}]}`)
inputBytes := len(lastRequest) + len(lastResponseOutput) + len(raw)
result := testing.Benchmark(func(b *testing.B) {
b.ReportAllocs()
for index := 0; index < b.N; index++ {
normalized, next, errMsg := normalizeResponsesWebsocketRequestWithMode(raw, lastRequest, lastResponseOutput, false, false)
if errMsg != nil {
b.Fatalf("unexpected error: %v", errMsg.Error)
}
runtime.KeepAlive(normalized)
runtime.KeepAlive(next)
}
})
const maxAllocationMultiple = 14
maxAllocatedBytes := int64(inputBytes * maxAllocationMultiple)
t.Logf("normalization allocated %d bytes per operation for %d input bytes", result.AllocedBytesPerOp(), inputBytes)
if allocatedBytes := result.AllocedBytesPerOp(); allocatedBytes > maxAllocatedBytes {
t.Fatalf("normalizing %d input bytes allocated %d bytes per operation, want at most %d", inputBytes, allocatedBytes, maxAllocatedBytes)
}
}
func TestResponsesWebsocketFallbackTurnBoundsTranscriptAllocations(t *testing.T) {
if raceDetectorEnabled {
t.Skip("allocation budgets are not meaningful with race detector instrumentation")
}
makeInput := func(count int, role string) string {
var input strings.Builder
input.WriteByte('[')
content := strings.Repeat("x", 1024)
for index := 0; index < count; index++ {
if index > 0 {
input.WriteByte(',')
}
fmt.Fprintf(&input, `{"type":"message","role":%q,"id":"%s-%d","content":%q}`, role, role, index, content)
}
input.WriteByte(']')
return input.String()
}
lastRequest := []byte(`{"model":"test-model","stream":true,"input":` + makeInput(128, "user") + `}`)
lastResponseOutput := []byte(makeInput(64, "assistant"))
raw := []byte(`{"type":"response.create","input":[{"type":"message","role":"user","id":"user-next","content":"continue"}]}`)
inputBytes := len(lastRequest) + len(lastResponseOutput) + len(raw)
result := testing.Benchmark(func(b *testing.B) {
b.ReportAllocs()
for index := 0; index < b.N; index++ {
requestJSON, _, errMsg := normalizeResponsesWebsocketRequestWithMode(raw, lastRequest, lastResponseOutput, false, false)
if errMsg != nil {
b.Fatalf("unexpected error: %v", errMsg.Error)
}
requestJSON, turn := prepareResponsesWebsocketFallbackTurn("allocation-session", requestJSON)
runtime.KeepAlive(requestJSON)
runtime.KeepAlive(turn)
}
})
const maxAllocationMultiple = 14
maxAllocatedBytes := int64(inputBytes * maxAllocationMultiple)
t.Logf("fallback turn allocated %d bytes per operation for %d input bytes", result.AllocedBytesPerOp(), inputBytes)
if allocatedBytes := result.AllocedBytesPerOp(); allocatedBytes > maxAllocatedBytes {
t.Fatalf("processing a fallback turn with %d input bytes allocated %d bytes per operation, want at most %d", inputBytes, allocatedBytes, maxAllocatedBytes)
}
}
func TestResponsesWebsocketToolCacheScansDoNotCopyLargePayloads(t *testing.T) {
const maxAllocatedBytes = 256 << 10
padding := strings.Repeat("x", 4<<20)
requestPayload := []byte(fmt.Sprintf(
`{"input":[{"type":"message","id":"message-1","call_id":"not-a-tool","content":%q},{"type":"function_call","id":"fc-1","call_id":"call-1","name":"lookup","arguments":"{}"},{"type":"function_call_output","id":"fco-1","call_id":"call-1","output":"ok"}]}`,
padding,
))
t.Run("request", func(t *testing.T) {
result := testing.Benchmark(func(b *testing.B) {
b.ReportAllocs()
for index := 0; index < b.N; index++ {
payload, turn := prepareResponsesWebsocketFallbackTurn("large-request-session", requestPayload)
runtime.KeepAlive(payload)
runtime.KeepAlive(turn)
}
})
t.Logf("request tool-cache scan allocated %d bytes per operation", result.AllocedBytesPerOp())
if allocatedBytes := result.AllocedBytesPerOp(); allocatedBytes > maxAllocatedBytes {
t.Fatalf("request tool-cache scan allocated %d bytes per operation, want at most %d", allocatedBytes, maxAllocatedBytes)
}
})
responsePayload := []byte(fmt.Sprintf(
`{"type":"response.completed","response":{"output":[{"type":"message","id":"message-1","content":%q},{"type":"function_call","id":"fc-1","call_id":"call-1","name":"lookup","arguments":"{}"}]}}`,
padding,
))
t.Run("response", func(t *testing.T) {
turn := newResponsesWebsocketToolCacheTurn("large-response-session")
result := testing.Benchmark(func(b *testing.B) {
b.ReportAllocs()
for index := 0; index < b.N; index++ {
turn.recordResponse(responsePayload)
runtime.KeepAlive(turn)
}
})
t.Logf("response tool-cache scan allocated %d bytes per operation", result.AllocedBytesPerOp())
if allocatedBytes := result.AllocedBytesPerOp(); allocatedBytes > maxAllocatedBytes {
t.Fatalf("response tool-cache scan allocated %d bytes per operation, want at most %d", allocatedBytes, maxAllocatedBytes)
}
})
}
func TestResponsesWebsocketToolCacheScanPreservesJSONRequestSemantics(t *testing.T) {
t.Run("rejects trailing data", func(t *testing.T) {
payload := []byte(`{"input":[{"type":"function_call","id":"fc-1","call_id":"call-1","name":"lookup","arguments":"{}"}]} trailing`)
repaired, turn := prepareResponsesWebsocketFallbackTurn("trailing-data-session", payload)
if !bytes.Equal(repaired, payload) {
t.Fatalf("repaired payload = %s, want original malformed payload", repaired)
}
if len(turn.calls) != 0 || len(turn.outputs) != 0 {
t.Fatalf("malformed payload recorded calls=%d outputs=%d, want none", len(turn.calls), len(turn.outputs))
}
})
t.Run("uses last duplicate input", func(t *testing.T) {
payload := []byte(`{"input":[{"type":"function_call","id":"fc-1","call_id":"call-1","name":"lookup","arguments":"{}"}],"input":[{"type":"message","id":"message-1","role":"user","content":"hello"}]}`)
repaired, turn := prepareResponsesWebsocketFallbackTurn("duplicate-input-session", payload)
if !bytes.Equal(repaired, payload) {
t.Fatalf("repaired payload = %s, want original payload", repaired)
}
if len(turn.calls) != 0 || len(turn.outputs) != 0 {
t.Fatalf("duplicate input recorded calls=%d outputs=%d from the shadowed value, want none", len(turn.calls), len(turn.outputs))
}
})
t.Run("repairs last case-insensitive duplicate input", func(t *testing.T) {
payload := []byte(`{"input":[{"type":"message","id":"shadowed","role":"user","content":"ignore"}],"INPUT":[{"type":"function_call_output","id":"fco-1","call_id":"missing-call","output":"orphan"}]}`)
repaired, _ := prepareResponsesWebsocketFallbackTurn("duplicate-case-input-session", payload)
var request struct {
Input []json.RawMessage `json:"input"`
}
if errUnmarshal := json.Unmarshal(repaired, &request); errUnmarshal != nil {
t.Fatalf("unmarshal repaired payload: %v", errUnmarshal)
}
if len(request.Input) != 0 {
t.Fatalf("repaired effective input count = %d, want 0", len(request.Input))
}
})
t.Run("repairs last exact duplicate input", func(t *testing.T) {
payload := []byte(`{"input":[{"type":"message","id":"shadowed","role":"user","content":"ignore"}],"input":[{"type":"function_call_output","id":"fco-1","call_id":"missing-call","output":"orphan"}]}`)
repaired, _ := prepareResponsesWebsocketFallbackTurn("duplicate-exact-input-session", payload)
var request struct {
Input []json.RawMessage `json:"input"`
}
if errUnmarshal := json.Unmarshal(repaired, &request); errUnmarshal != nil {
t.Fatalf("unmarshal repaired payload: %v", errUnmarshal)
}
if len(request.Input) != 0 {
t.Fatalf("repaired effective input count = %d, want 0", len(request.Input))
}
})
t.Run("rejects invalid earlier duplicate input", func(t *testing.T) {
payload := []byte(`{"input":{},"input":[{"type":"function_call","id":"fc-1","call_id":"call-1","name":"lookup","arguments":"{}"},{"type":"function_call_output","id":"fco-1","call_id":"call-1","output":"ok"}]}`)
repaired, turn := prepareResponsesWebsocketFallbackTurn("invalid-duplicate-input-session", payload)
if !bytes.Equal(repaired, payload) {
t.Fatalf("repaired payload = %s, want original payload with invalid duplicate input", repaired)
}
if len(turn.calls) != 0 || len(turn.outputs) != 0 {
t.Fatalf("invalid duplicate input recorded calls=%d outputs=%d, want none", len(turn.calls), len(turn.outputs))
}
})
t.Run("uses last duplicate previous response id", func(t *testing.T) {
payload := []byte(`{"previous_response_id":"resp-first","previous_response_id":null,"input":[{"type":"function_call_output","id":"fco-1","call_id":"missing-call","output":"orphan"}]}`)
repaired, _ := prepareResponsesWebsocketFallbackTurn("duplicate-previous-response-session", payload)
if inputCount := gjson.GetBytes(repaired, "input.#").Int(); inputCount != 0 {
t.Fatalf("repaired input count = %d, want 0 when the last previous_response_id is null", inputCount)
}
})
}
func TestNormalizeResponsesWebsocketRequestCreateWithHistory(t *testing.T) {
lastRequest := []byte(`{"model":"test-model","stream":true,"input":[{"type":"message","id":"msg-1"}]}`)
lastResponseOutput := []byte(`[
{"type":"function_call","id":"fc-1","call_id":"call-1"},
{"type":"message","id":"assistant-1"}
]`)
raw := []byte(`{"type":"response.create","input":[{"type":"function_call_output","call_id":"call-1","id":"tool-out-1"}]}`)
normalized, next, errMsg := normalizeResponsesWebsocketRequest(raw, lastRequest, lastResponseOutput)
if errMsg != nil {
t.Fatalf("unexpected error: %v", errMsg.Error)
}
if gjson.GetBytes(normalized, "type").Exists() {
t.Fatalf("normalized subsequent create request must not include type field")
}
if gjson.GetBytes(normalized, "model").String() != "test-model" {
t.Fatalf("unexpected model: %s", gjson.GetBytes(normalized, "model").String())
}
input := gjson.GetBytes(normalized, "input").Array()
if len(input) != 4 {
t.Fatalf("merged input len = %d, want 4", len(input))
}
if input[0].Get("id").String() != "msg-1" ||
input[1].Get("id").String() != "fc-1" ||
input[2].Get("id").String() != "assistant-1" ||
input[3].Get("id").String() != "tool-out-1" {
t.Fatalf("unexpected merged input order")
}
if !bytes.Equal(next, normalized) {
t.Fatalf("next request snapshot should match normalized request")
}
}
func TestNormalizeResponsesWebsocketRequestWithPreviousResponseIDIncremental(t *testing.T) {
lastRequest := []byte(`{"model":"test-model","stream":true,"instructions":"be helpful","input":[{"type":"message","id":"msg-1"}]}`)
lastResponseOutput := []byte(`[
{"type":"function_call","id":"fc-1","call_id":"call-1"},
{"type":"message","id":"assistant-1"}
]`)
raw := []byte(`{"type":"response.create","previous_response_id":"resp-1","input":[{"type":"function_call_output","call_id":"call-1","id":"tool-out-1"}]}`)
normalized, next, errMsg := normalizeResponsesWebsocketRequestWithMode(raw, lastRequest, lastResponseOutput, true, false)
if errMsg != nil {
t.Fatalf("unexpected error: %v", errMsg.Error)
}
if gjson.GetBytes(normalized, "type").Exists() {
t.Fatalf("normalized request must not include type field")
}
if gjson.GetBytes(normalized, "previous_response_id").String() != "resp-1" {
t.Fatalf("previous_response_id must be preserved in incremental mode")
}
input := gjson.GetBytes(normalized, "input").Array()
if len(input) != 1 {
t.Fatalf("incremental input len = %d, want 1", len(input))
}
if input[0].Get("id").String() != "tool-out-1" {
t.Fatalf("unexpected incremental input item id: %s", input[0].Get("id").String())
}
if gjson.GetBytes(normalized, "model").String() != "test-model" {
t.Fatalf("unexpected model: %s", gjson.GetBytes(normalized, "model").String())
}
if gjson.GetBytes(normalized, "instructions").String() != "be helpful" {
t.Fatalf("unexpected instructions: %s", gjson.GetBytes(normalized, "instructions").String())
}
if !bytes.Equal(next, normalized) {
t.Fatalf("next request snapshot should match normalized request")
}
}
func TestNormalizeResponsesWebsocketRequestInjectsPreviousResponseIDForIncremental(t *testing.T) {
lastRequest := []byte(`{"model":"test-model","stream":true,"instructions":"be helpful","input":[{"type":"message","id":"msg-1"}]}`)
lastResponseOutput := []byte(`[
{"type":"function_call","id":"fc-1","call_id":"call-1"},
{"type":"message","id":"assistant-1"}
]`)
raw := []byte(`{"type":"response.create","input":[{"type":"function_call_output","call_id":"call-1","id":"tool-out-1"}]}`)
normalized, next, errMsg := normalizeResponsesWebsocketRequestWithLastResponseID(raw, lastRequest, lastResponseOutput, "resp-1", true, false)
if errMsg != nil {
t.Fatalf("unexpected error: %v", errMsg.Error)
}
if got := gjson.GetBytes(normalized, "previous_response_id").String(); got != "resp-1" {
t.Fatalf("previous_response_id = %q, want resp-1", got)
}
input := gjson.GetBytes(normalized, "input").Array()
if len(input) != 1 {
t.Fatalf("incremental input len = %d, want 1: %s", len(input), normalized)
}
if input[0].Get("id").String() != "tool-out-1" {
t.Fatalf("unexpected incremental input item id: %s", input[0].Get("id").String())
}
if gjson.GetBytes(normalized, "model").String() != "test-model" {
t.Fatalf("unexpected model: %s", gjson.GetBytes(normalized, "model").String())
}
if gjson.GetBytes(normalized, "instructions").String() != "be helpful" {
t.Fatalf("unexpected instructions: %s", gjson.GetBytes(normalized, "instructions").String())
}
if !bytes.Equal(next, normalized) {
t.Fatalf("next request snapshot should match normalized request")
}
}
func TestNormalizeResponsesWebsocketRequestInjectsPreviousResponseIDWhenPendingOutputIsPresent(t *testing.T) {
lastRequest := []byte(`{"model":"test-model","stream":true,"instructions":"be helpful","input":[{"type":"message","id":"msg-1"}]}`)
lastResponseOutput := []byte(`[]`)
raw := []byte(`{"type":"response.create","input":[{"type":"function_call_output","call_id":"call-1","id":"tool-out-1"}]}`)
normalized, _, errMsg := normalizeResponsesWebsocketRequestWithIncrementalState(raw, lastRequest, lastResponseOutput, "resp-1", []string{"call-1"}, true, false)
if errMsg != nil {
t.Fatalf("unexpected error: %v", errMsg.Error)
}
if got := gjson.GetBytes(normalized, "previous_response_id").String(); got != "resp-1" {
t.Fatalf("previous_response_id = %q, want resp-1", got)
}
input := gjson.GetBytes(normalized, "input").Array()
if len(input) != 1 || input[0].Get("id").String() != "tool-out-1" {
t.Fatalf("unexpected incremental input: %s", normalized)
}
}
func TestNormalizeResponsesWebsocketRequestSkipsPreviousResponseIDWhenPendingOutputIsMissing(t *testing.T) {
lastRequest := []byte(`{"model":"test-model","stream":true,"instructions":"be helpful","input":[{"type":"message","id":"msg-1"}]}`)
lastResponseOutput := []byte(`[
{"type":"function_call","id":"fc-1","call_id":"call-1"}
]`)
raw := []byte(`{"type":"response.create","input":[{"type":"message","role":"user","id":"summary-1","content":"compacted summary"}]}`)
normalized, next, errMsg := normalizeResponsesWebsocketRequestWithIncrementalState(raw, lastRequest, lastResponseOutput, "resp-1", []string{"call-1"}, true, false)
if errMsg != nil {
t.Fatalf("unexpected error: %v", errMsg.Error)
}
if gjson.GetBytes(normalized, "previous_response_id").Exists() {
t.Fatalf("previous_response_id must not be injected when pending tool output is missing: %s", normalized)
}
input := gjson.GetBytes(normalized, "input").Array()
if len(input) != 1 {
t.Fatalf("replacement input len = %d, want 1: %s", len(input), normalized)
}
if input[0].Get("id").String() != "summary-1" {
t.Fatalf("unexpected replacement input: %s", normalized)
}
if !bytes.Equal(next, normalized) {
t.Fatalf("next request snapshot should match normalized request")
}
}
func TestNormalizeResponsesWebsocketRequestReplacesCodexLocalCompactionTranscript(t *testing.T) {
lastRequest := []byte(`{"model":"gpt-5.6-sol","stream":true,"instructions":"be helpful","input":[
{"type":"message","role":"user","id":"old-user","content":[{"type":"input_text","text":"old prompt"}]},
{"type":"function_call_output","id":"old-tool-output","call_id":"old-call","output":"old result"}
]}`)
lastResponseOutput := []byte(`[
{"type":"function_call","id":"old-tool-call","call_id":"old-call","name":"lookup","arguments":"{}"},
{"type":"message","role":"assistant","id":"old-assistant","content":[{"type":"output_text","text":"old answer"}]}
]`)
raw := []byte(fmt.Sprintf(`{"type":"response.create","input":[
{"type":"additional_tools","role":"developer","tools":[]},
{"role":"developer","id":"initial-context","content":"workspace context"},
{"type":"message","role":"user","id":"compacted-user","content":[{"type":"input_text","text":"retained context"}]},
{"role":"user","id":"local-summary","content":%q},
{"type":"message","role":"developer","id":"turn-context","content":[{"type":"input_text","text":"current workspace context"}]},
{"role":"user","id":"incoming-user","content":"continue the task"}
],"parallel_tool_calls":true,"client_metadata":{"ws_request_header_x_openai_internal_codex_responses_lite":"true"}}`, codexLocalCompactionSummaryPrefix+"\nThe compacted summary."))
normalized, next, errMsg := normalizeResponsesWebsocketRequestWithMode(raw, lastRequest, lastResponseOutput, false, false)
if errMsg != nil {
t.Fatalf("unexpected error: %v", errMsg.Error)
}
if gjson.GetBytes(normalized, "previous_response_id").Exists() {
t.Fatalf("replacement request must not include previous_response_id: %s", normalized)
}
if got, want := gjson.GetBytes(normalized, "input").Raw, gjson.GetBytes(raw, "input").Raw; got != want {
t.Fatalf("replacement input did not preserve the complete new transcript:\n got: %s\nwant: %s", got, want)
}
input := gjson.GetBytes(normalized, "input").Array()
wantIDs := []string{"", "initial-context", "compacted-user", "local-summary", "turn-context", "incoming-user"}
if len(input) != len(wantIDs) {
t.Fatalf("replacement input len = %d, want %d: %s", len(input), len(wantIDs), normalized)
}
for index, wantID := range wantIDs {
if got := input[index].Get("id").String(); got != wantID {
t.Fatalf("replacement input[%d].id = %q, want %q: %s", index, got, wantID, normalized)
}
}
if got := input[0].Get("type").String(); got != "additional_tools" {
t.Fatalf("input[0].type = %q, want additional_tools: %s", got, normalized)
}
if got := input[0].Get("role").String(); got != "developer" {
t.Fatalf("input[0].role = %q, want developer: %s", got, normalized)
}
if tools := input[0].Get("tools"); !tools.IsArray() || len(tools.Array()) != 0 {
t.Fatalf("input[0] empty tools array was not preserved: %s", normalized)
}
for _, staleID := range []string{"old-user", "old-tool-output", "old-tool-call", "old-assistant"} {
if bytes.Contains(normalized, []byte(staleID)) {
t.Fatalf("replacement input contains stale item %q: %s", staleID, normalized)
}
}
if got := gjson.GetBytes(normalized, "model").String(); got != "gpt-5.6-sol" {
t.Fatalf("model = %q, want gpt-5.6-sol", got)
}
if got := gjson.GetBytes(normalized, "instructions").String(); got != "be helpful" {
t.Fatalf("instructions = %q, want be helpful", got)
}
if !gjson.GetBytes(normalized, "stream").Bool() {
t.Fatalf("stream must be enabled: %s", normalized)
}
if !gjson.GetBytes(normalized, "parallel_tool_calls").Bool() {
t.Fatalf("parallel_tool_calls was not preserved: %s", normalized)
}
if got := gjson.GetBytes(normalized, "client_metadata.ws_request_header_x_openai_internal_codex_responses_lite").String(); got != "true" {
t.Fatalf("Responses Lite client metadata = %q, want true: %s", got, normalized)
}
if !bytes.Equal(next, normalized) {
t.Fatalf("next request snapshot should match normalized request")
}
}
func TestShouldReplaceWebsocketTranscriptCodexLocalCompactionSemantics(t *testing.T) {
compactedInput := gjson.Parse(fmt.Sprintf(`[
{"type":"message","role":"developer","content":[{"type":"input_text","text":"initial context"}]},
{"type":"message","role":"user","content":[{"type":"input_text","text":"retained context"}]},
{"type":"message","role":"user","content":[{"type":"input_text","text":%q}]}
]`, codexLocalCompactionSummaryPrefix+"\nSummary body."))
if !shouldReplaceWebsocketTranscript([]byte(`{"type":"response.create"}`), compactedInput) {
t.Fatal("Codex local compaction input must replace the websocket transcript")
}
for _, request := range []string{
`{"type":"response.create","previous_response_id":"resp-1"}`,
`{"type":"response.create","previous_response_id":""}`,
`{"type":"response.create","previous_response_id":null}`,
} {
if shouldReplaceWebsocketTranscript([]byte(request), compactedInput) {
t.Fatalf("request carrying previous_response_id must not use the local compaction rule: %s", request)
}
}
if shouldReplaceWebsocketTranscript([]byte(`{"type":"response.append"}`), compactedInput) {
t.Fatal("response.append must not be treated as a full local compaction reset")
}
ordinaryInput := gjson.Parse(`[
{"type":"message","role":"developer","content":"Please summarize future messages."},
{"type":"message","role":"user","content":[{"type":"input_text","text":"Please create a compacted summary of this text."}]}
]`)
if shouldReplaceWebsocketTranscript([]byte(`{"type":"response.create"}`), ordinaryInput) {
t.Fatal("ordinary user/developer input must not replace the transcript")
}
}
func TestCodexLocalCompactionSummaryContentShapes(t *testing.T) {
tests := []struct {
name string
content string
want bool
}{
{name: "string content", content: fmt.Sprintf(`%q`, codexLocalCompactionSummaryPrefix+"\nSummary body."), want: true},
{name: "multiple input text parts", content: fmt.Sprintf(`[{"type":"input_text","text":%q},{"type":"input_text","text":"\nSummary body."}]`, codexLocalCompactionSummaryPrefix), want: true},
{name: "non-text part before summary", content: fmt.Sprintf(`[{"type":"input_image","image_url":"data:image/png;base64,AA=="},{"type":"input_text","text":%q}]`, codexLocalCompactionSummaryPrefix+"\nSummary body."), want: true},
{name: "bare prefix", content: fmt.Sprintf(`%q`, codexLocalCompactionSummaryPrefix), want: false},
{name: "prefix followed by space", content: fmt.Sprintf(`%q`, codexLocalCompactionSummaryPrefix+" Summary body."), want: false},
{name: "summary after ordinary text", content: fmt.Sprintf(`[{"type":"input_text","text":"ordinary text"},{"type":"input_text","text":%q}]`, codexLocalCompactionSummaryPrefix+"\nSummary body."), want: false},
{name: "developer summary", content: fmt.Sprintf(`%q`, codexLocalCompactionSummaryPrefix+"\nSummary body."), want: false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
role := "user"
if test.name == "developer summary" {
role = "developer"
}
input := gjson.Parse(fmt.Sprintf(`[{"type":"message","role":%q,"content":%s}]`, role, test.content))
if got := inputHasCodexLocalCompactionSummary(input); got != test.want {
t.Fatalf("inputHasCodexLocalCompactionSummary() = %t, want %t", got, test.want)
}
})
}
}
func TestCodexLocalCompactionSummaryAdditionalToolsConstraints(t *testing.T) {
summary := fmt.Sprintf(`{"role":"user","content":%q}`, codexLocalCompactionSummaryPrefix+"\nSummary body.")
tests := []struct {
name string
input string
want bool
}{
{name: "Responses Lite tools first", input: fmt.Sprintf(`[{"type":"additional_tools","role":"developer","tools":[{"type":"custom","name":"exec"}]},%s]`, summary), want: true},
{name: "tools after message", input: fmt.Sprintf(`[%s,{"type":"additional_tools","role":"developer","tools":[{"type":"custom","name":"exec"}]}]`, summary)},
{name: "tools with user role", input: fmt.Sprintf(`[{"type":"additional_tools","role":"user","tools":[{"type":"custom","name":"exec"}]},%s]`, summary)},
{name: "tools missing array", input: fmt.Sprintf(`[{"type":"additional_tools","role":"developer"},%s]`, summary)},
{name: "tools not array", input: fmt.Sprintf(`[{"type":"additional_tools","role":"developer","tools":{}},%s]`, summary)},
{name: "tools empty", input: fmt.Sprintf(`[{"type":"additional_tools","role":"developer","tools":[]},%s]`, summary), want: true},
{name: "malformed tool", input: fmt.Sprintf(`[{"type":"additional_tools","role":"developer","tools":[null]},%s]`, summary)},
{name: "arbitrary input item", input: fmt.Sprintf(`[{"type":"unknown","role":"developer"},%s]`, summary)},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := inputHasCodexLocalCompactionSummary(gjson.Parse(test.input)); got != test.want {
t.Fatalf("inputHasCodexLocalCompactionSummary() = %t, want %t", got, test.want)
}
})
}
}
func TestCodexLocalCompactionSummaryRejectsOrdinaryHistoryItems(t *testing.T) {
tests := []struct {
name string
historyItem string
wantReplace bool
}{
{name: "reasoning", historyItem: `{"type":"reasoning","id":"reasoning-1"}`},
{name: "assistant", historyItem: `{"type":"message","role":"assistant","id":"assistant-1"}`, wantReplace: true},
{name: "function call", historyItem: `{"type":"function_call","call_id":"call-1"}`, wantReplace: true},
{name: "function call output", historyItem: `{"type":"function_call_output","call_id":"call-1"}`},
{name: "custom tool call", historyItem: `{"type":"custom_tool_call","call_id":"call-1"}`, wantReplace: true},
{name: "custom tool call output", historyItem: `{"type":"custom_tool_call_output","call_id":"call-1"}`},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
input := gjson.Parse(fmt.Sprintf(`[%s,{"type":"message","role":"user","content":[{"type":"input_text","text":%q}]}]`, test.historyItem, codexLocalCompactionSummaryPrefix+"\nSummary body."))
if inputHasCodexLocalCompactionSummary(input) {
t.Fatal("ordinary transcript history must not match the local user-summary shape")
}
if got := shouldReplaceWebsocketTranscript([]byte(`{"type":"response.create"}`), input); got != test.wantReplace {
t.Fatalf("shouldReplaceWebsocketTranscript() = %t, want %t", got, test.wantReplace)
}
})
}
}
func TestNormalizeResponsesWebsocketRequestWithPreviousResponseIDMergedWhenIncrementalDisabled(t *testing.T) {
lastRequest := []byte(`{"model":"test-model","stream":true,"input":[{"type":"message","id":"msg-1"}]}`)
lastResponseOutput := []byte(`[
{"type":"function_call","id":"fc-1","call_id":"call-1"},
{"type":"message","id":"assistant-1"}
]`)
raw := []byte(`{"type":"response.create","previous_response_id":"resp-1","input":[{"type":"function_call_output","call_id":"call-1","id":"tool-out-1"}]}`)
normalized, next, errMsg := normalizeResponsesWebsocketRequestWithMode(raw, lastRequest, lastResponseOutput, false, false)
if errMsg != nil {
t.Fatalf("unexpected error: %v", errMsg.Error)
}
if gjson.GetBytes(normalized, "previous_response_id").Exists() {
t.Fatalf("previous_response_id must be removed when incremental mode is disabled")
}
input := gjson.GetBytes(normalized, "input").Array()
if len(input) != 4 {
t.Fatalf("merged input len = %d, want 4", len(input))
}
if input[0].Get("id").String() != "msg-1" ||
input[1].Get("id").String() != "fc-1" ||
input[2].Get("id").String() != "assistant-1" ||
input[3].Get("id").String() != "tool-out-1" {
t.Fatalf("unexpected merged input order")
}
if !bytes.Equal(next, normalized) {
t.Fatalf("next request snapshot should match normalized request")
}
}
func TestNormalizeResponsesWebsocketRequestAppend(t *testing.T) {
lastRequest := []byte(`{"model":"test-model","stream":true,"input":[{"type":"message","id":"msg-1"}]}`)
lastResponseOutput := []byte(`[
{"type":"message","id":"assistant-1"},
{"type":"function_call_output","id":"tool-out-1"}
]`)
raw := []byte(`{"type":"response.append","input":[{"type":"message","id":"msg-2"},{"type":"message","id":"msg-3"}]}`)
normalized, next, errMsg := normalizeResponsesWebsocketRequest(raw, lastRequest, lastResponseOutput)
if errMsg != nil {
t.Fatalf("unexpected error: %v", errMsg.Error)
}
input := gjson.GetBytes(normalized, "input").Array()
if len(input) != 5 {
t.Fatalf("merged input len = %d, want 5", len(input))
}
if input[0].Get("id").String() != "msg-1" ||
input[1].Get("id").String() != "assistant-1" ||
input[2].Get("id").String() != "tool-out-1" ||
input[3].Get("id").String() != "msg-2" ||
input[4].Get("id").String() != "msg-3" {
t.Fatalf("unexpected merged input order")
}
if !bytes.Equal(next, normalized) {
t.Fatalf("next request snapshot should match normalized append request")
}
}
func TestNormalizeResponsesWebsocketRequestAppendWithoutCreate(t *testing.T) {
raw := []byte(`{"type":"response.append","input":[]}`)
_, _, errMsg := normalizeResponsesWebsocketRequest(raw, nil, nil)
if errMsg == nil {
t.Fatalf("expected error for append without previous request")
}
if errMsg.StatusCode != http.StatusBadRequest {
t.Fatalf("status = %d, want %d", errMsg.StatusCode, http.StatusBadRequest)
}
}
func TestWebsocketJSONPayloadsFromChunk(t *testing.T) {
chunk := []byte("event: response.created\n\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp-1\"}}\n\ndata: [DONE]\n")
payloads := websocketJSONPayloadsFromChunk(chunk)
if len(payloads) != 1 {
t.Fatalf("payloads len = %d, want 1", len(payloads))
}
if gjson.GetBytes(payloads[0], "type").String() != "response.created" {
t.Fatalf("unexpected payload type: %s", gjson.GetBytes(payloads[0], "type").String())
}
}
func TestWebsocketJSONPayloadsFromPlainJSONChunk(t *testing.T) {
chunk := []byte(`{"type":"response.completed","response":{"id":"resp-1"}}`)
payloads := websocketJSONPayloadsFromChunk(chunk)
if len(payloads) != 1 {
t.Fatalf("payloads len = %d, want 1", len(payloads))
}
if gjson.GetBytes(payloads[0], "type").String() != "response.completed" {
t.Fatalf("unexpected payload type: %s", gjson.GetBytes(payloads[0], "type").String())
}
}
func TestResponseCompletedOutputFromPayload(t *testing.T) {
payload := []byte(`{"type":"response.completed","response":{"id":"resp-1","output":[{"type":"message","id":"out-1"}]}}`)
output := responseCompletedOutputFromPayload(payload, nil, nil)
items := gjson.ParseBytes(output).Array()
if len(items) != 1 {
t.Fatalf("output len = %d, want 1", len(items))
}
if items[0].Get("id").String() != "out-1" {
t.Fatalf("unexpected output id: %s", items[0].Get("id").String())
}
}
func TestResponseCompletedOutputFromPayloadDropsIncompleteCollectedToolCalls(t *testing.T) {
payload := []byte(`{"type":"response.completed","response":{"id":"resp-1","output":[]}}`)
collector := map[int64][]byte{
0: []byte(`{"type":"message","id":"msg-1"}`),
1: []byte(`{"type":"function_call","call_id":"call-1","name":"exec"}`),
2: []byte(`{"type":"custom_tool_call","call_id":"call-2","name":"exec","input":"pwd"}`),
}
output := responseCompletedOutputFromPayload(payload, collector, nil)
items := gjson.ParseBytes(output).Array()
if len(items) != 2 {
t.Fatalf("output len = %d, want 2: %s", len(items), output)
}
if items[0].Get("type").String() != "message" || items[0].Get("id").String() != "msg-1" {
t.Fatalf("unexpected first output item: %s", items[0].Raw)
}
if items[1].Get("type").String() != "custom_tool_call" || items[1].Get("call_id").String() != "call-2" {
t.Fatalf("unexpected second output item: %s", items[1].Raw)
}
}
func TestRestoreResponsesWebsocketCompletionOutputPreservesNonEmptyOutput(t *testing.T) {
payload := []byte(`{"type":"response.completed","response":{"id":"resp-1","output":[{"type":"message","id":"out-1"}]}}`)
collector := map[int64][]byte{0: []byte(`{"type":"function_call","id":"call-1","call_id":"call-1"}`)}
restored := restoreResponsesWebsocketCompletionOutput(payload, collector, nil)
if string(restored) != string(payload) {
t.Fatalf("non-empty completion output was overwritten: %s", restored)
}
}
func TestRestoreResponsesWebsocketCompletionOutputReconcilesConflictingToolCall(t *testing.T) {
payload := []byte(`{"type":"response.completed","response":{"id":"resp-1","output":[{"type":"message","id":"msg-1"},{"type":"function_call","call_id":"call-1","name":"exec"}]}}`)
collector := map[int64][]byte{0: []byte(`{"type":"custom_tool_call","id":"ctc-1","call_id":"call-1","name":"exec","input":"pwd","status":"completed"}`)}
restored := restoreResponsesWebsocketCompletionOutput(payload, collector, nil)
output := gjson.GetBytes(restored, "response.output").Array()
if len(output) != 2 {
t.Fatalf("restored output len = %d, want 2: %s", len(output), restored)
}
if output[0].Get("type").String() != "message" || output[0].Get("id").String() != "msg-1" {
t.Fatalf("unrelated completion item changed: %s", output[0].Raw)
}
if output[1].Get("type").String() != "custom_tool_call" || output[1].Get("call_id").String() != "call-1" {
t.Fatalf("conflicting tool call was not reconciled: %s", output[1].Raw)
}
if input := output[1].Get("input"); input.Type != gjson.String || input.String() != "pwd" {
t.Fatalf("reconciled custom tool input = %s, want string pwd", input.Raw)
}
lastRequest := []byte(`{"model":"gpt-test","stream":true,"input":[{"type":"message","id":"user-1","role":"user","content":"run pwd"}]}`)
nextRequest := []byte(`{"type":"response.create","previous_response_id":"resp-1","input":[{"type":"custom_tool_call_output","call_id":"call-1","output":"ok"}]}`)
completedOutput := []byte(gjson.GetBytes(restored, "response.output").Raw)
normalized, _, errMsg := normalizeResponsesWebsocketRequestWithIncrementalState(
nextRequest,
lastRequest,
completedOutput,
"resp-1",
[]string{"call-1"},
false,
false,
)
if errMsg != nil {
t.Fatalf("normalize next request: %v", errMsg.Error)
}
if gjson.GetBytes(normalized, "previous_response_id").Exists() {
t.Fatalf("previous_response_id must not be forwarded to HTTP/SSE upstream: %s", normalized)
}
input := gjson.GetBytes(normalized, "input").Array()
if len(input) != 4 {
t.Fatalf("replayed input len = %d, want 4: %s", len(input), normalized)
}
if input[2].Get("type").String() != "custom_tool_call" || input[2].Get("input").String() != "pwd" {
t.Fatalf("replayed tool call is invalid: %s", input[2].Raw)
}
if input[3].Get("type").String() != "custom_tool_call_output" || input[3].Get("call_id").String() != "call-1" {
t.Fatalf("replayed tool output is invalid: %s", input[3].Raw)
}
cache := newWebsocketToolOutputCache(time.Minute, 10)
donePayload := []byte(`{"type":"response.output_item.done","item":{"type":"custom_tool_call","id":"ctc-1","call_id":"call-1","name":"exec","input":"pwd","status":"completed"}}`)
recordResponsesWebsocketToolCallsFromPayloadWithCache(cache, "session-1", donePayload)
recordResponsesWebsocketToolCallsFromPayloadWithCache(cache, "session-1", restored)
cached, ok := cache.get("session-1", "call-1")
if !ok {
t.Fatalf("reconciled custom tool call was not cached")
}
if gjson.GetBytes(cached, "type").String() != "custom_tool_call" || gjson.GetBytes(cached, "input").String() != "pwd" {
t.Fatalf("cached tool call is invalid: %s", cached)
}
}
func TestRestoreResponsesWebsocketCompletionOutputIgnoresIncompleteCollectedToolCall(t *testing.T) {
payload := []byte(`{"type":"response.completed","response":{"id":"resp-1","output":[{"type":"function_call","call_id":"call-1","name":"exec"}]}}`)
collector := map[int64][]byte{0: []byte(`{"type":"custom_tool_call","call_id":"call-1","name":"exec"}`)}
restored := restoreResponsesWebsocketCompletionOutput(payload, collector, nil)
if string(restored) != string(payload) {
t.Fatalf("incomplete collected tool call overwrote completion output: %s", restored)
}
}
func TestIsCompleteResponsesWebsocketToolCallRequiresStringFields(t *testing.T) {
tests := []struct {
name string
item string
want bool
}{
{name: "numeric call id", item: `{"type":"function_call","call_id":123,"name":"exec","arguments":"{}"}`},
{name: "boolean name", item: `{"type":"function_call","call_id":"call-1","name":true,"arguments":"{}"}`},
{name: "numeric arguments", item: `{"type":"function_call","call_id":"call-1","name":"exec","arguments":123}`},
{name: "object custom input", item: `{"type":"custom_tool_call","call_id":"call-1","name":"exec","input":{}}`},
{name: "valid function call", item: `{"type":"function_call","call_id":"call-1","name":"exec","arguments":""}`, want: true},
{name: "valid custom tool call", item: `{"type":"custom_tool_call","call_id":"call-1","name":"exec","input":""}`, want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isCompleteResponsesWebsocketToolCall(gjson.Parse(tt.item)); got != tt.want {
t.Fatalf("isCompleteResponsesWebsocketToolCall() = %t, want %t", got, tt.want)
}
})
}
}
func TestAppendWebsocketEvent(t *testing.T) {
var builder strings.Builder
appendWebsocketEvent(&builder, "request", []byte(" {\"type\":\"response.create\"}\n"))
appendWebsocketEvent(&builder, "response", []byte("{\"type\":\"response.created\"}"))
got := builder.String()
if !strings.Contains(got, "websocket.request\n{\"type\":\"response.create\"}\n") {
t.Fatalf("request event not found in body: %s", got)
}
if !strings.Contains(got, "websocket.response\n{\"type\":\"response.created\"}\n") {
t.Fatalf("response event not found in body: %s", got)
}
}
func TestAppendWebsocketTimelineEvent(t *testing.T) {
var builder strings.Builder
ts := time.Date(2026, time.April, 1, 12, 34, 56, 789000000, time.UTC)
appendWebsocketTimelineEvent(&builder, "request", []byte(" {\"type\":\"response.create\"}\n"), ts)
got := builder.String()
if !strings.Contains(got, "Timestamp: 2026-04-01T12:34:56.789Z") {
t.Fatalf("timeline timestamp not found: %s", got)
}
if !strings.Contains(got, "Event: websocket.request") {
t.Fatalf("timeline event not found: %s", got)
}
if !strings.Contains(got, "{\"type\":\"response.create\"}") {
t.Fatalf("timeline payload not found: %s", got)
}
}
func TestSetWebsocketTimelineBody(t *testing.T) {
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
setWebsocketTimelineBody(c, " \n ")
if _, exists := c.Get(wsTimelineBodyKey); exists {
t.Fatalf("timeline body key should not be set for empty body")
}
setWebsocketTimelineBody(c, "timeline body")
value, exists := c.Get(wsTimelineBodyKey)
if !exists {
t.Fatalf("timeline body key not set")
}
bodyBytes, ok := value.([]byte)
if !ok {
t.Fatalf("timeline body key type mismatch")
}
if string(bodyBytes) != "timeline body" {
t.Fatalf("timeline body = %q, want %q", string(bodyBytes), "timeline body")
}
}
func TestWebsocketTimelineLogFallsBackToMemoryWithoutSource(t *testing.T) {
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
ts := time.Date(2026, time.April, 1, 12, 34, 56, 789000000, time.UTC)
timelineLog := newWebsocketTimelineLog(true, nil)
timelineLog.BeginRequest()
timelineLog.Append("request", []byte(`{"type":"response.create"}`), ts)
timelineLog.SetContext(c)
value, exists := c.Get(wsTimelineBodyKey)
if !exists {
t.Fatalf("timeline body key not set")
}
bodyBytes, ok := value.([]byte)
if !ok {
t.Fatalf("timeline body key type mismatch")
}
got := string(bodyBytes)
if !strings.Contains(got, "Event: websocket.request") {
t.Fatalf("timeline event not found: %s", got)
}
if !strings.Contains(got, `{"type":"response.create"}`) {
t.Fatalf("timeline payload not found: %s", got)
}
}
func TestRepairResponsesWebsocketToolCallsInsertsCachedOutput(t *testing.T) {
cache := newWebsocketToolOutputCache(time.Minute, 10)
sessionKey := "session-1"
cacheWarm := []byte(`{"previous_response_id":"resp-1","input":[{"type":"function_call_output","call_id":"call-1","output":"ok"}]}`)
warmed := repairResponsesWebsocketToolCallsWithCache(cache, sessionKey, cacheWarm)
if gjson.GetBytes(warmed, "input.0.call_id").String() != "call-1" {
t.Fatalf("expected warmup output to remain")
}
raw := []byte(`{"input":[{"type":"function_call","call_id":"call-1","name":"tool"},{"type":"message","id":"msg-1"}]}`)
repaired := repairResponsesWebsocketToolCallsWithCache(cache, sessionKey, raw)
input := gjson.GetBytes(repaired, "input").Array()
if len(input) != 3 {
t.Fatalf("repaired input len = %d, want 3", len(input))
}
if input[0].Get("type").String() != "function_call" || input[0].Get("call_id").String() != "call-1" {
t.Fatalf("unexpected first item: %s", input[0].Raw)
}
if input[1].Get("type").String() != "function_call_output" || input[1].Get("call_id").String() != "call-1" {
t.Fatalf("missing inserted output: %s", input[1].Raw)
}
if input[2].Get("type").String() != "message" || input[2].Get("id").String() != "msg-1" {
t.Fatalf("unexpected trailing item: %s", input[2].Raw)
}
}
func TestRepairResponsesWebsocketToolCallsDeduplicatesInputItemsByID(t *testing.T) {
cache := newWebsocketToolOutputCache(time.Minute, 10)
raw := []byte(`{"input":[{"type":"message","id":"msg-1","content":"old"},{"type":"message","id":"msg-1","content":"new"}]}`)
for _, sessionKey := range []string{"dedupe-session", ""} {
t.Run(fmt.Sprintf("session_key_%q", sessionKey), func(t *testing.T) {
repaired := repairResponsesWebsocketToolCallsWithCache(cache, sessionKey, raw)
items := gjson.GetBytes(repaired, "input").Array()
if len(items) != 1 {
t.Fatalf("repaired input len = %d, want 1: %s", len(items), repaired)
}
if got := items[0].Get("content").String(); got != "new" {
t.Fatalf("repaired input content = %q, want new: %s", got, repaired)
}
})
}
}
func TestResponsesWebsocketToolCacheTurnDoesNotRetainRequestBackingStorage(t *testing.T) {
const (
paddingSize = 32 << 20
maxRetainedHeap = 8 << 20
)
runtime.GC()
var before runtime.MemStats
runtime.ReadMemStats(&before)
prefix := []byte(`{"input":[{"type":"message","id":"padding","content":"`)
suffix := []byte(`"},{"type":"function_call_output","id":"fco-1","call_id":"call-1","output":"ok"}]}`)
payload := make([]byte, len(prefix)+paddingSize+len(suffix))
copy(payload, prefix)
for index := len(prefix); index < len(prefix)+paddingSize; index++ {
payload[index] = 'x'
}
copy(payload[len(prefix)+paddingSize:], suffix)
repaired, turn := prepareResponsesWebsocketFallbackTurn("backing-storage-session", payload)
payload = nil
repaired = nil
runtime.GC()
runtime.GC()
var after runtime.MemStats
runtime.ReadMemStats(&after)
runtime.KeepAlive(turn)
runtime.KeepAlive(repaired)
retainedHeap := int64(after.HeapAlloc) - int64(before.HeapAlloc)
if retainedHeap > maxRetainedHeap {
t.Fatalf("tool cache turn retained %d bytes after request release, want at most %d", retainedHeap, maxRetainedHeap)
}
}
func TestResponsesWebsocketToolCacheTurnCommitsOnlyOnSuccess(t *testing.T) {
const sessionKey = "tool-cache-turn-commit-session"
defer defaultWebsocketToolOutputCache.deleteSession(sessionKey)
defer defaultWebsocketToolCallCache.deleteSession(sessionKey)
_, turn := prepareResponsesWebsocketFallbackTurn(sessionKey, []byte(`{"input":[{"type":"function_call_output","id":"fco-1","call_id":"call-1","output":"cached result"}]}`))
beforeCommit := repairResponsesWebsocketToolCallsWithoutRecording(sessionKey, []byte(`{"input":[{"type":"function_call","id":"fc-next","call_id":"call-1","name":"lookup","arguments":"{}"}]}`))
if gjson.GetBytes(beforeCommit, "input.#").Int() != 0 {
t.Fatalf("uncommitted turn populated global cache: %s", beforeCommit)
}
turn.commit()
afterCommit := repairResponsesWebsocketToolCallsWithoutRecording(sessionKey, []byte(`{"input":[{"type":"function_call","id":"fc-next","call_id":"call-1","name":"lookup","arguments":"{}"}]}`))
input := gjson.GetBytes(afterCommit, "input").Array()
if len(input) != 2 || input[1].Get("output").String() != "cached result" {
t.Fatalf("committed turn was not available to tool repair: %s", afterCommit)
}
}
func TestResponsesWebsocketToolCacheRetainPreventsOverlappingReleaseDeletion(t *testing.T) {
const sessionKey = "tool-cache-overlapping-retain-session"
retainResponsesWebsocketToolCaches(sessionKey)
retainResponsesWebsocketToolCaches(sessionKey)
_, turn := prepareResponsesWebsocketFallbackTurn(sessionKey, []byte(`{"input":[{"type":"function_call_output","id":"fco-1","call_id":"call-1","output":"kept"}]}`))
turn.commit()
releaseResponsesWebsocketToolCaches(sessionKey)
if _, ok := defaultWebsocketToolOutputCache.get(sessionKey, "call-1"); !ok {
t.Fatal("first overlapping release deleted active session cache")
}
releaseResponsesWebsocketToolCaches(sessionKey)
if _, ok := defaultWebsocketToolOutputCache.get(sessionKey, "call-1"); ok {
t.Fatal("final release did not delete session cache")
}
}
func TestRepairResponsesWebsocketToolCallsDropsOrphanFunctionCall(t *testing.T) {
cache := newWebsocketToolOutputCache(time.Minute, 10)
sessionKey := "session-1"
raw := []byte(`{"input":[{"type":"function_call","call_id":"call-1","name":"tool"},{"type":"message","id":"msg-1"}]}`)
repaired := repairResponsesWebsocketToolCallsWithCache(cache, sessionKey, raw)
input := gjson.GetBytes(repaired, "input").Array()
if len(input) != 1 {
t.Fatalf("repaired input len = %d, want 1", len(input))
}
if input[0].Get("type").String() != "message" || input[0].Get("id").String() != "msg-1" {
t.Fatalf("unexpected remaining item: %s", input[0].Raw)
}
}
func TestRepairResponsesWebsocketToolCallsInsertsCachedCallForOrphanOutput(t *testing.T) {
outputCache := newWebsocketToolOutputCache(time.Minute, 10)
callCache := newWebsocketToolOutputCache(time.Minute, 10)
sessionKey := "session-1"
callCache.record(sessionKey, "call-1", []byte(`{"type":"function_call","call_id":"call-1","name":"tool"}`))
raw := []byte(`{"input":[{"type":"function_call_output","call_id":"call-1","output":"ok"},{"type":"message","id":"msg-1"}]}`)
repaired := repairResponsesWebsocketToolCallsWithCaches(outputCache, callCache, sessionKey, raw)
input := gjson.GetBytes(repaired, "input").Array()
if len(input) != 3 {
t.Fatalf("repaired input len = %d, want 3", len(input))
}
if input[0].Get("type").String() != "function_call" || input[0].Get("call_id").String() != "call-1" {
t.Fatalf("missing inserted call: %s", input[0].Raw)
}
if input[1].Get("type").String() != "function_call_output" || input[1].Get("call_id").String() != "call-1" {
t.Fatalf("unexpected output item: %s", input[1].Raw)
}
if input[2].Get("type").String() != "message" || input[2].Get("id").String() != "msg-1" {
t.Fatalf("unexpected trailing item: %s", input[2].Raw)
}
}
func TestRepairResponsesWebsocketToolCallsKeepsPreviousResponseOutputIncremental(t *testing.T) {
outputCache := newWebsocketToolOutputCache(time.Minute, 10)
callCache := newWebsocketToolOutputCache(time.Minute, 10)
sessionKey := "session-1"
callCache.record(sessionKey, "call-1", []byte(`{"type":"function_call","id":"fc-1","call_id":"call-1","name":"tool"}`))
raw := []byte(`{"previous_response_id":"resp-latest","input":[{"type":"function_call_output","call_id":"call-1","id":"tool-out-1","output":"ok"},{"type":"message","id":"msg-1"}]}`)
repaired := repairResponsesWebsocketToolCallsWithCaches(outputCache, callCache, sessionKey, raw)
if got := gjson.GetBytes(repaired, "previous_response_id").String(); got != "resp-latest" {
t.Fatalf("previous_response_id = %q, want resp-latest", got)
}
input := gjson.GetBytes(repaired, "input").Array()
if len(input) != 2 {
t.Fatalf("repaired input len = %d, want 2: %s", len(input), repaired)
}
if input[0].Get("type").String() != "function_call_output" || input[0].Get("call_id").String() != "call-1" {
t.Fatalf("unexpected output item: %s", input[0].Raw)
}
if input[1].Get("type").String() != "message" || input[1].Get("id").String() != "msg-1" {
t.Fatalf("unexpected trailing item: %s", input[1].Raw)
}
}
func TestRepairResponsesWebsocketToolCallsKeepsPreviousResponseCallIncremental(t *testing.T) {
outputCache := newWebsocketToolOutputCache(time.Minute, 10)
callCache := newWebsocketToolOutputCache(time.Minute, 10)
sessionKey := "session-1"
outputCache.record(sessionKey, "call-1", []byte(`{"type":"function_call_output","call_id":"call-1","id":"tool-out-1","output":"ok"}`))
raw := []byte(`{"previous_response_id":"resp-latest","input":[{"type":"function_call","id":"fc-1","call_id":"call-1","name":"tool"},{"type":"message","id":"msg-1"}]}`)
repaired := repairResponsesWebsocketToolCallsWithCaches(outputCache, callCache, sessionKey, raw)
if got := gjson.GetBytes(repaired, "previous_response_id").String(); got != "resp-latest" {
t.Fatalf("previous_response_id = %q, want resp-latest", got)
}
input := gjson.GetBytes(repaired, "input").Array()
if len(input) != 2 {
t.Fatalf("repaired input len = %d, want 2: %s", len(input), repaired)
}
if input[0].Get("type").String() != "function_call" || input[0].Get("call_id").String() != "call-1" {
t.Fatalf("unexpected call item: %s", input[0].Raw)
}
if input[1].Get("type").String() != "message" || input[1].Get("id").String() != "msg-1" {
t.Fatalf("unexpected trailing item: %s", input[1].Raw)
}
}
func TestRepairResponsesWebsocketToolCallsDropsOrphanOutputWhenCallMissing(t *testing.T) {
outputCache := newWebsocketToolOutputCache(time.Minute, 10)
callCache := newWebsocketToolOutputCache(time.Minute, 10)
sessionKey := "session-1"
raw := []byte(`{"input":[{"type":"function_call_output","call_id":"call-1","output":"ok"},{"type":"message","id":"msg-1"}]}`)
repaired := repairResponsesWebsocketToolCallsWithCaches(outputCache, callCache, sessionKey, raw)
input := gjson.GetBytes(repaired, "input").Array()
if len(input) != 1 {
t.Fatalf("repaired input len = %d, want 1", len(input))
}
if input[0].Get("type").String() != "message" || input[0].Get("id").String() != "msg-1" {
t.Fatalf("unexpected remaining item: %s", input[0].Raw)
}
}
func TestRepairResponsesWebsocketToolCallsInsertsCachedCustomToolOutput(t *testing.T) {
cache := newWebsocketToolOutputCache(time.Minute, 10)
sessionKey := "session-1"
cacheWarm := []byte(`{"previous_response_id":"resp-1","input":[{"type":"custom_tool_call_output","call_id":"call-1","output":"ok"}]}`)
warmed := repairResponsesWebsocketToolCallsWithCache(cache, sessionKey, cacheWarm)
if gjson.GetBytes(warmed, "input.0.call_id").String() != "call-1" {
t.Fatalf("expected warmup output to remain")
}
raw := []byte(`{"input":[{"type":"custom_tool_call","call_id":"call-1","name":"apply_patch"},{"type":"message","id":"msg-1"}]}`)
repaired := repairResponsesWebsocketToolCallsWithCache(cache, sessionKey, raw)
input := gjson.GetBytes(repaired, "input").Array()
if len(input) != 3 {
t.Fatalf("repaired input len = %d, want 3", len(input))
}
if input[0].Get("type").String() != "custom_tool_call" || input[0].Get("call_id").String() != "call-1" {
t.Fatalf("unexpected first item: %s", input[0].Raw)
}
if input[1].Get("type").String() != "custom_tool_call_output" || input[1].Get("call_id").String() != "call-1" {
t.Fatalf("missing inserted output: %s", input[1].Raw)
}
if input[2].Get("type").String() != "message" || input[2].Get("id").String() != "msg-1" {
t.Fatalf("unexpected trailing item: %s", input[2].Raw)
}
}
func TestRepairResponsesWebsocketToolCallsDropsOrphanCustomToolCall(t *testing.T) {
cache := newWebsocketToolOutputCache(time.Minute, 10)
sessionKey := "session-1"
raw := []byte(`{"input":[{"type":"custom_tool_call","call_id":"call-1","name":"apply_patch"},{"type":"message","id":"msg-1"}]}`)
repaired := repairResponsesWebsocketToolCallsWithCache(cache, sessionKey, raw)
input := gjson.GetBytes(repaired, "input").Array()
if len(input) != 1 {
t.Fatalf("repaired input len = %d, want 1", len(input))
}
if input[0].Get("type").String() != "message" || input[0].Get("id").String() != "msg-1" {
t.Fatalf("unexpected remaining item: %s", input[0].Raw)
}
}
func TestRepairResponsesWebsocketToolCallsInsertsCachedCustomToolCallForOrphanOutput(t *testing.T) {
outputCache := newWebsocketToolOutputCache(time.Minute, 10)
callCache := newWebsocketToolOutputCache(time.Minute, 10)
sessionKey := "session-1"
callCache.record(sessionKey, "call-1", []byte(`{"type":"custom_tool_call","call_id":"call-1","name":"apply_patch"}`))
raw := []byte(`{"input":[{"type":"custom_tool_call_output","call_id":"call-1","output":"ok"},{"type":"message","id":"msg-1"}]}`)
repaired := repairResponsesWebsocketToolCallsWithCaches(outputCache, callCache, sessionKey, raw)
input := gjson.GetBytes(repaired, "input").Array()
if len(input) != 3 {
t.Fatalf("repaired input len = %d, want 3", len(input))
}
if input[0].Get("type").String() != "custom_tool_call" || input[0].Get("call_id").String() != "call-1" {
t.Fatalf("missing inserted call: %s", input[0].Raw)
}
if input[1].Get("type").String() != "custom_tool_call_output" || input[1].Get("call_id").String() != "call-1" {
t.Fatalf("unexpected output item: %s", input[1].Raw)
}
if input[2].Get("type").String() != "message" || input[2].Get("id").String() != "msg-1" {
t.Fatalf("unexpected trailing item: %s", input[2].Raw)
}
}
func TestRepairResponsesWebsocketToolCallsKeepsPreviousResponseCustomToolOutputIncremental(t *testing.T) {
outputCache := newWebsocketToolOutputCache(time.Minute, 10)
callCache := newWebsocketToolOutputCache(time.Minute, 10)
sessionKey := "session-1"
callCache.record(sessionKey, "call-1", []byte(`{"type":"custom_tool_call","call_id":"call-1","name":"apply_patch"}`))
raw := []byte(`{"previous_response_id":"resp-latest","input":[{"type":"custom_tool_call_output","call_id":"call-1","output":"ok"},{"type":"message","id":"msg-1"}]}`)
repaired := repairResponsesWebsocketToolCallsWithCaches(outputCache, callCache, sessionKey, raw)
if got := gjson.GetBytes(repaired, "previous_response_id").String(); got != "resp-latest" {
t.Fatalf("previous_response_id = %q, want resp-latest", got)
}
input := gjson.GetBytes(repaired, "input").Array()
if len(input) != 2 {
t.Fatalf("repaired input len = %d, want 2: %s", len(input), repaired)
}
if input[0].Get("type").String() != "custom_tool_call_output" || input[0].Get("call_id").String() != "call-1" {
t.Fatalf("unexpected output item: %s", input[0].Raw)
}
if input[1].Get("type").String() != "message" || input[1].Get("id").String() != "msg-1" {
t.Fatalf("unexpected trailing item: %s", input[1].Raw)
}
}
func TestRepairResponsesWebsocketToolCallsDropsOrphanCustomToolOutputWhenCallMissing(t *testing.T) {
outputCache := newWebsocketToolOutputCache(time.Minute, 10)
callCache := newWebsocketToolOutputCache(time.Minute, 10)
sessionKey := "session-1"
raw := []byte(`{"input":[{"type":"custom_tool_call_output","call_id":"call-1","output":"ok"},{"type":"message","id":"msg-1"}]}`)
repaired := repairResponsesWebsocketToolCallsWithCaches(outputCache, callCache, sessionKey, raw)
input := gjson.GetBytes(repaired, "input").Array()
if len(input) != 1 {
t.Fatalf("repaired input len = %d, want 1", len(input))
}
if input[0].Get("type").String() != "message" || input[0].Get("id").String() != "msg-1" {
t.Fatalf("unexpected remaining item: %s", input[0].Raw)
}
}
func TestRecordResponsesWebsocketToolCallsIgnoresIncompleteCall(t *testing.T) {
cache := newWebsocketToolOutputCache(time.Minute, 10)
pending := make(map[string]struct{})
payload := []byte(`{"type":"response.output_item.done","item":{"type":"function_call","call_id":"call-1","name":"exec"}}`)
recordResponsesWebsocketToolCallsFromPayloadWithCache(cache, "session-1", payload)
recordPendingToolCallIDsFromPayload(pending, payload)
if cached, ok := cache.get("session-1", "call-1"); ok {
t.Fatalf("incomplete tool call was cached: %s", cached)
}
if len(pending) != 0 {
t.Fatalf("incomplete tool call was recorded as pending: %v", pending)
}
}
func TestRecordResponsesWebsocketToolCallsFromPayloadWithCache(t *testing.T) {
cache := newWebsocketToolOutputCache(time.Minute, 10)
sessionKey := "session-1"
payload := []byte(`{"type":"response.completed","response":{"id":"resp-1","output":[{"type":"function_call","id":"fc-1","call_id":"call-1","name":"tool","arguments":"{}"}]}}`)
recordResponsesWebsocketToolCallsFromPayloadWithCache(cache, sessionKey, payload)
cached, ok := cache.get(sessionKey, "call-1")
if !ok {
t.Fatalf("expected cached tool call")
}
if gjson.GetBytes(cached, "type").String() != "function_call" || gjson.GetBytes(cached, "call_id").String() != "call-1" {
t.Fatalf("unexpected cached tool call: %s", cached)
}
}
func TestRecordResponsesWebsocketCustomToolCallsFromCompletedPayloadWithCache(t *testing.T) {
cache := newWebsocketToolOutputCache(time.Minute, 10)
sessionKey := "session-1"
payload := []byte(`{"type":"response.completed","response":{"id":"resp-1","output":[{"type":"custom_tool_call","id":"ctc-1","call_id":"call-1","name":"apply_patch","input":"*** Begin Patch"}]}}`)
recordResponsesWebsocketToolCallsFromPayloadWithCache(cache, sessionKey, payload)
cached, ok := cache.get(sessionKey, "call-1")
if !ok {
t.Fatalf("expected cached custom tool call")
}
if gjson.GetBytes(cached, "type").String() != "custom_tool_call" || gjson.GetBytes(cached, "call_id").String() != "call-1" {
t.Fatalf("unexpected cached custom tool call: %s", cached)
}
}
func TestRecordResponsesWebsocketCustomToolCallsFromOutputItemDoneWithCache(t *testing.T) {
cache := newWebsocketToolOutputCache(time.Minute, 10)
sessionKey := "session-1"
payload := []byte(`{"type":"response.output_item.done","item":{"type":"custom_tool_call","id":"ctc-1","call_id":"call-1","name":"apply_patch","input":"*** Begin Patch"}}`)
recordResponsesWebsocketToolCallsFromPayloadWithCache(cache, sessionKey, payload)
cached, ok := cache.get(sessionKey, "call-1")
if !ok {
t.Fatalf("expected cached custom tool call")
}
if gjson.GetBytes(cached, "type").String() != "custom_tool_call" || gjson.GetBytes(cached, "call_id").String() != "call-1" {
t.Fatalf("unexpected cached custom tool call: %s", cached)
}
}
func TestForwardResponsesWebsocketRestoresAndForwardsCompletedOutput(t *testing.T) {
gin.SetMode(gin.TestMode)
serverErrCh := make(chan error, 1)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := responsesWebsocketUpgrader.Upgrade(w, r, nil)
if err != nil {
serverErrCh <- err
return
}
defer func() {
errClose := conn.Close()
if errClose != nil {
serverErrCh <- errClose
}
}()
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
ctx.Request = r
data := make(chan []byte, 2)
errCh := make(chan *interfaces.ErrorMessage)
data <- []byte(`{"type":"response.output_item.done","output_index":0,"item":{"type":"function_call","id":"call-1","call_id":"call-1","name":"lookup","arguments":"{}"}}`)
data <- []byte("data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp-1\",\"output\":[]}}\n\n")
close(data)
close(errCh)
timelineLog := newInMemoryWebsocketTimelineLog()
completedOutput, completedResponseID, pendingToolCallIDs, errMsg, err := (*OpenAIResponsesAPIHandler)(nil).forwardResponsesWebsocket(
ctx,
newResponsesWebsocketWriter(conn),
func(...interface{}) {},
data,
errCh,
timelineLog,
"session-1",
)
if err != nil {
serverErrCh <- err
return
}
if errMsg != nil {
serverErrCh <- fmt.Errorf("unexpected websocket error message: %v", errMsg.Error)
return
}
if gjson.GetBytes(completedOutput, "0.id").String() != "call-1" {
serverErrCh <- errors.New("completed output not restored")
return
}
if completedResponseID != "resp-1" {
serverErrCh <- fmt.Errorf("completed response id = %q, want resp-1", completedResponseID)
return
}
if len(pendingToolCallIDs) != 1 || pendingToolCallIDs[0] != "call-1" {
serverErrCh <- fmt.Errorf("pending tool call ids = %v, want [call-1]", pendingToolCallIDs)
return
}
if !strings.Contains(timelineLog.String(), "Event: websocket.response") {
serverErrCh <- errors.New("websocket timeline did not capture downstream response")
return
}
serverErrCh <- nil
}))
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() {
errClose := conn.Close()
if errClose != nil {
t.Fatalf("close websocket: %v", errClose)
}
}()
_, outputItemPayload, errReadMessage := conn.ReadMessage()
if errReadMessage != nil {
t.Fatalf("read output item websocket message: %v", errReadMessage)
}
if got := gjson.GetBytes(outputItemPayload, "type").String(); got != "response.output_item.done" {
t.Fatalf("output item payload type = %s, want response.output_item.done", got)
}
_, payload, errReadMessage := conn.ReadMessage()
if errReadMessage != nil {
t.Fatalf("read completion websocket message: %v", errReadMessage)
}
if gjson.GetBytes(payload, "type").String() != wsEventTypeCompleted {
t.Fatalf("payload type = %s, want %s", gjson.GetBytes(payload, "type").String(), wsEventTypeCompleted)
}
if strings.Contains(string(payload), "response.done") {
t.Fatalf("payload unexpectedly rewrote completed event: %s", payload)
}
if got := gjson.GetBytes(payload, "response.output.0.id").String(); got != "call-1" {
t.Fatalf("downstream completion output id = %q, want call-1; payload=%s", got, payload)
}
if errServer := <-serverErrCh; errServer != nil {
t.Fatalf("server error: %v", errServer)
}
}
func TestForwardResponsesWebsocketTreatsResponseDoneAsTerminalWithoutRewriting(t *testing.T) {
gin.SetMode(gin.TestMode)
serverErrCh := make(chan error, 1)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := responsesWebsocketUpgrader.Upgrade(w, r, nil)
if err != nil {
serverErrCh <- err
return
}
defer func() {
errClose := conn.Close()
if errClose != nil {
serverErrCh <- errClose
}
}()
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
ctx.Request = r
data := make(chan []byte, 1)
errCh := make(chan *interfaces.ErrorMessage)
data <- []byte(`{"type":"response.done","response":{"id":"resp-1","output":[{"type":"message","id":"out-1"}]}}`)
close(data)
close(errCh)
timelineLog := newInMemoryWebsocketTimelineLog()
completedOutput, completedResponseID, pendingToolCallIDs, errMsg, err := (*OpenAIResponsesAPIHandler)(nil).forwardResponsesWebsocket(
ctx,
newResponsesWebsocketWriter(conn),
func(...interface{}) {},
data,
errCh,
timelineLog,
"session-1",
)
if err != nil {
serverErrCh <- err
return
}
if errMsg != nil {
serverErrCh <- fmt.Errorf("unexpected websocket error message: %v", errMsg.Error)
return
}
if gjson.GetBytes(completedOutput, "0.id").String() != "out-1" {
serverErrCh <- errors.New("done output not captured")
return
}
if completedResponseID != "resp-1" {
serverErrCh <- fmt.Errorf("completed response id = %q, want resp-1", completedResponseID)
return
}
if len(pendingToolCallIDs) != 0 {
serverErrCh <- fmt.Errorf("pending tool call ids = %v, want empty", pendingToolCallIDs)
return
}
serverErrCh <- nil
}))
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() {
errClose := conn.Close()
if errClose != nil {
t.Fatalf("close websocket: %v", errClose)
}
}()
_, payload, errReadMessage := conn.ReadMessage()
if errReadMessage != nil {
t.Fatalf("read websocket message: %v", errReadMessage)
}
if got := gjson.GetBytes(payload, "type").String(); got != "response.done" {
t.Fatalf("payload type = %s, want response.done; payload=%s", got, payload)
}
if errServer := <-serverErrCh; errServer != nil {
t.Fatalf("server error: %v", errServer)
}
}
func TestShouldExposeResponsesUpstreamError(t *testing.T) {
tests := []struct {
status int
want bool
}{
{status: http.StatusBadRequest, want: true},
{status: http.StatusConflict, want: true},
{status: http.StatusRequestEntityTooLarge, want: true},
{status: http.StatusUnprocessableEntity, want: true},
{status: http.StatusUnauthorized},
{status: http.StatusRequestTimeout},
{status: http.StatusTooManyRequests},
{status: http.StatusInternalServerError},
}
for _, tc := range tests {
t.Run(strconv.Itoa(tc.status), func(t *testing.T) {
errMsg := &interfaces.ErrorMessage{StatusCode: tc.status, Error: errors.New(http.StatusText(tc.status))}
if got := shouldExposeResponsesUpstreamError(errMsg); got != tc.want {
t.Fatalf("shouldExposeResponsesUpstreamError(%d) = %t, want %t", tc.status, got, tc.want)
}
})
}
}
// TestResponsesUpstreamErrorBodyDrivesExposure pins that the error body, not the
// attached status, decides whether a request-shape failure is exposed. Codex
// reports the same cyber_policy rejection as 400 on the stream error path and as
// 502 through the websocket disconnect channel.
func TestResponsesUpstreamErrorBodyDrivesExposure(t *testing.T) {
tests := []struct {
name string
status int
body string
want bool
}{
{name: "bad request", status: http.StatusBadRequest, body: "bad request", want: true},
{name: "conflict", status: http.StatusConflict, body: "conflict", want: true},
{name: "entity too large", status: http.StatusRequestEntityTooLarge, body: "too large", want: true},
{name: "unprocessable", status: http.StatusUnprocessableEntity, body: "unprocessable", want: true},
{
name: "cyber policy at 502",
status: http.StatusBadGateway,
body: `{"error":{"type":"invalid_request","code":"cyber_policy","message":"flagged"}}`,
want: true,
},
{
name: "context length exceeded at 500",
status: http.StatusInternalServerError,
body: `{"error":{"type":"invalid_request_error","code":"context_length_exceeded","message":"too long"}}`,
want: true,
},
// Credential, quota and transport failures stay silent: the client just
// reconnects, and a fresh socket already implies a full context resend.
{name: "unauthorized", status: http.StatusUnauthorized, body: "invalid token"},
{name: "payment required", status: http.StatusPaymentRequired, body: "insufficient credits"},
{name: "forbidden", status: http.StatusForbidden, body: "forbidden"},
{name: "too many requests", status: http.StatusTooManyRequests, body: "usage limit reached"},
{name: "request timeout", status: http.StatusRequestTimeout, body: "timeout"},
{name: "bad gateway", status: http.StatusBadGateway, body: "bad gateway"},
{
name: "upstream websocket drop",
status: http.StatusInternalServerError,
body: `{"error":{"message":"websocket: close 1006 (abnormal closure): unexpected EOF","type":"server_error","code":"internal_server_error"}}`,
},
{name: "no error message", status: 0, body: ""},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
errMsg := &interfaces.ErrorMessage{StatusCode: tc.status}
if tc.body != "" {
errMsg.Error = errors.New(tc.body)
}
if got := shouldExposeResponsesUpstreamError(errMsg); got != tc.want {
t.Fatalf("shouldExposeResponsesUpstreamError = %t, want %t", got, tc.want)
}
})
}
if shouldExposeResponsesUpstreamError(nil) {
t.Fatal("nil error message must not be exposed")
}
}
func TestForwardResponsesWebsocketTreatsErrorPayloadAsTerminal(t *testing.T) {
gin.SetMode(gin.TestMode)
serverErrCh := make(chan error, 1)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := responsesWebsocketUpgrader.Upgrade(w, r, nil)
if err != nil {
serverErrCh <- err
return
}
defer func() { _ = conn.Close() }()
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
ctx.Request = r
data := make(chan []byte, 1)
errCh := make(chan *interfaces.ErrorMessage)
data <- []byte(`{"type":"error","status":400,"error":{"type":"invalid_request_error","message":"invalid request"}}`)
close(data)
close(errCh)
_, _, _, errMsg, err := (*OpenAIResponsesAPIHandler)(nil).forwardResponsesWebsocket(
ctx,
newResponsesWebsocketWriter(conn),
func(...interface{}) {},
data,
errCh,
newInMemoryWebsocketTimelineLog(),
"session-1",
)
if err != nil && !errors.Is(err, websocket.ErrCloseSent) {
serverErrCh <- err
return
}
if errMsg == nil {
serverErrCh <- errors.New("expected websocket error message")
return
}
if errMsg.StatusCode != http.StatusBadRequest {
serverErrCh <- fmt.Errorf("websocket error status = %d, want %d", errMsg.StatusCode, http.StatusBadRequest)
return
}
if errMsg.Error == nil || !strings.Contains(errMsg.Error.Error(), "invalid request") {
serverErrCh <- fmt.Errorf("websocket error = %v, want invalid request", errMsg.Error)
return
}
serverErrCh <- nil
}))
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() {
errClose := conn.Close()
if errClose != nil {
t.Fatalf("close websocket: %v", errClose)
}
}()
_, payload, errReadMessage := conn.ReadMessage()
if errReadMessage != nil {
t.Fatalf("read websocket message: %v", errReadMessage)
}
if got := gjson.GetBytes(payload, "type").String(); got != wsEventTypeError {
t.Fatalf("payload type = %s, want %s; payload=%s", got, wsEventTypeError, payload)
}
if errServer := <-serverErrCh; errServer != nil {
t.Fatalf("server error: %v", errServer)
}
}
func TestRecordPendingToolCallIDsFromPayloadDropsSatisfiedCalls(t *testing.T) {
pending := map[string]struct{}{}
payload := []byte(`{"type":"response.completed","response":{"output":[{"type":"function_call","call_id":"call-1","id":"fc-1"},{"type":"function_call_output","call_id":"call-1","id":"out-1"},{"type":"custom_tool_call","call_id":"call-2","id":"ctc-1"},{"type":"custom_tool_call_output","call_id":"call-2","id":"custom-out-1"}]}}`)
recordPendingToolCallIDsFromPayload(pending, payload)
if len(pending) != 0 {
t.Fatalf("pending tool call ids = %v, want empty", sortedStringSet(pending))
}
}
func TestForwardResponsesWebsocketLogsAttemptedResponseOnWriteFailure(t *testing.T) {
gin.SetMode(gin.TestMode)
serverErrCh := make(chan error, 1)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := responsesWebsocketUpgrader.Upgrade(w, r, nil)
if err != nil {
serverErrCh <- err
return
}
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
ctx.Request = r
data := make(chan []byte, 1)
errCh := make(chan *interfaces.ErrorMessage)
data <- []byte("data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp-1\",\"output\":[{\"type\":\"message\",\"id\":\"out-1\"}]}}\n\n")
close(data)
close(errCh)
timelineLog := newInMemoryWebsocketTimelineLog()
if errClose := conn.Close(); errClose != nil {
serverErrCh <- errClose
return
}
_, _, _, _, err = (*OpenAIResponsesAPIHandler)(nil).forwardResponsesWebsocket(
ctx,
newResponsesWebsocketWriter(conn),
func(...interface{}) {},
data,
errCh,
timelineLog,
"session-1",
)
if err == nil {
serverErrCh <- errors.New("expected websocket write failure")
return
}
if !strings.Contains(timelineLog.String(), "Event: websocket.response") {
serverErrCh <- errors.New("websocket timeline did not capture attempted downstream response")
return
}
if !strings.Contains(timelineLog.String(), "\"type\":\"response.completed\"") {
serverErrCh <- errors.New("websocket timeline did not retain attempted payload")
return
}
serverErrCh <- nil
}))
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() {
_ = conn.Close()
}()
if errServer := <-serverErrCh; errServer != nil {
t.Fatalf("server error: %v", errServer)
}
}
func TestResponsesWebsocketTimelineRecordsDisconnectEvent(t *testing.T) {
gin.SetMode(gin.TestMode)
manager := coreauth.NewManager(nil, nil, nil)
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{RequestLog: true}, manager)
h := NewOpenAIResponsesAPIHandler(base)
logsDir := t.TempDir()
timelineCh := make(chan string, 1)
router := gin.New()
router.GET("/v1/responses/ws", func(c *gin.Context) {
source, errSource := requestlogging.NewFileBodySourceInDir(logsDir, "websocket-timeline-test")
if errSource != nil {
timelineCh <- ""
return
}
c.Set(requestlogging.WebsocketTimelineSourceContextKey, source)
h.ResponsesWebsocket(c)
timeline := ""
if value, exists := c.Get(wsTimelineBodyKey); exists {
if body, ok := value.([]byte); ok {
timeline = string(body)
}
} else if value, exists := c.Get(requestlogging.WebsocketTimelineSourceContextKey); exists {
if source, ok := value.(*requestlogging.FileBodySource); ok {
body, _ := source.Bytes()
timeline = string(body)
_ = source.Cleanup()
}
}
if value, exists := c.Get(requestlogging.APIWebsocketTimelineSourceContextKey); exists {
if source, ok := value.(*requestlogging.FileBodySource); ok {
_ = source.Cleanup()
}
}
timelineCh <- timeline
})
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
closePayload := websocket.FormatCloseMessage(websocket.CloseGoingAway, "client closing")
if err = conn.WriteControl(websocket.CloseMessage, closePayload, time.Now().Add(time.Second)); err != nil {
t.Fatalf("write close control: %v", err)
}
_ = conn.Close()
select {
case timeline := <-timelineCh:
if !strings.Contains(timeline, "Event: websocket.disconnect") {
t.Fatalf("websocket timeline missing disconnect event: %s", timeline)
}
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for websocket timeline")
}
}
func TestResponsesWebsocketMirrorsUpstreamMessageTooBigDisconnect(t *testing.T) {
gin.SetMode(gin.TestMode)
for _, provider := range []string{"codex", "xai"} {
t.Run(provider, func(t *testing.T) {
executor := &websocketUpstreamDisconnectExecutor{provider: provider, subscribed: make(chan string, 1)}
manager := coreauth.NewManager(nil, nil, nil)
manager.RegisterExecutor(executor)
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
var sessionID string
select {
case sessionID = <-executor.subscribed:
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for upstream disconnect subscription")
}
executor.TriggerDisconnect(sessionID, &websocket.CloseError{
Code: websocket.CloseMessageTooBig,
Text: "message too big",
})
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
_, _, err = conn.ReadMessage()
var closeErr *websocket.CloseError
if !errors.As(err, &closeErr) {
t.Fatalf("expected downstream websocket close error, got %v", err)
}
if closeErr.Code != websocket.CloseMessageTooBig {
t.Fatalf("downstream close code = %d, want %d", closeErr.Code, websocket.CloseMessageTooBig)
}
if closeErr.Text != "message too big" {
t.Fatalf("downstream close reason = %q, want message too big", closeErr.Text)
}
})
}
}
func TestResponsesWebsocketSendsJSONErrorOnUpstreamCyberPolicyDisconnect(t *testing.T) {
gin.SetMode(gin.TestMode)
executor := &websocketUpstreamDisconnectExecutor{provider: "codex", subscribed: make(chan string, 1)}
manager := coreauth.NewManager(nil, nil, nil)
manager.RegisterExecutor(executor)
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
var sessionID string
select {
case sessionID = <-executor.subscribed:
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for upstream disconnect subscription")
}
cyberPolicyErr := websocketPinnedFailoverStatusError{
status: http.StatusBadRequest,
msg: `{"error":{"type":"invalid_request","code":"cyber_policy","message":"This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. To get authorized for security work, join the Trusted Access for Cyber program: https://chatgpt.com/cyber","param":null}}`,
}
executor.TriggerDisconnect(sessionID, cyberPolicyErr)
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
msgType, payload, err := conn.ReadMessage()
if err != nil {
t.Fatalf("expected downstream text error payload before socket close, got read error: %v", err)
}
if msgType != websocket.TextMessage {
t.Fatalf("msgType = %d, want TextMessage (%d)", msgType, websocket.TextMessage)
}
if gjson.GetBytes(payload, "type").String() != "error" {
t.Fatalf("payload type = %q, want %q", gjson.GetBytes(payload, "type").String(), "error")
}
if status := int(gjson.GetBytes(payload, "status").Int()); status != http.StatusBadRequest {
t.Fatalf("status = %d, want %d", status, http.StatusBadRequest)
}
if gjson.GetBytes(payload, "error.code").String() != "cyber_policy" {
t.Fatalf("error.code = %q, want %q", gjson.GetBytes(payload, "error.code").String(), "cyber_policy")
}
if !strings.Contains(gjson.GetBytes(payload, "error.message").String(), "cybersecurity risk") {
t.Fatalf("error.message = %q, want cybersecurity risk text", gjson.GetBytes(payload, "error.message").String())
}
if _, duplicate, errRead := conn.ReadMessage(); errRead == nil {
t.Fatalf("received duplicate error frame: %s", duplicate)
}
}
func TestResponsesWebsocketHidesNonClientUpstreamDisconnectErrors(t *testing.T) {
gin.SetMode(gin.TestMode)
tests := []struct {
name string
err error
}{
{
name: "abnormal closure",
err: &websocket.CloseError{
Code: websocket.CloseAbnormalClosure,
Text: "unexpected EOF",
},
},
{
name: "upstream read timeout",
err: errors.New("read tcp 198.18.0.1:53030->145.223.58.12:6281: i/o timeout"),
},
{
// Credential failover already ran and lost; the client only needs to
// reconnect, so no downstream error is produced.
name: "quota exhausted",
err: websocketPinnedFailoverStatusError{
status: http.StatusTooManyRequests,
msg: `{"error":{"type":"usage_limit_reached","message":"The usage limit has been reached"}}`,
},
},
{
name: "credential rejected",
err: websocketPinnedFailoverStatusError{
status: http.StatusUnauthorized,
msg: `{"error":{"type":"authentication_error","message":"Invalid token"}}`,
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
executor := &websocketUpstreamDisconnectExecutor{provider: "codex", subscribed: make(chan string, 1)}
manager := coreauth.NewManager(nil, nil, nil)
manager.RegisterExecutor(executor)
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
var sessionID string
select {
case sessionID = <-executor.subscribed:
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for upstream disconnect subscription")
}
executor.TriggerDisconnect(sessionID, tc.err)
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
_, payload, errRead := conn.ReadMessage()
if errRead == nil {
t.Fatalf("non-client upstream error was exposed: %s", payload)
}
// Nothing may be written downstream: no error frame and no close frame
// carrying proxy-internal detail, so the client just reconnects.
var closeErr *websocket.CloseError
if errors.As(errRead, &closeErr) && closeErr.Code != websocket.CloseAbnormalClosure {
t.Fatalf("non-client upstream error produced a close frame: %#v", closeErr)
}
})
}
}
// TestResponsesWebsocketExposesCyberPolicyRegardlessOfStatus pins the other
// production shape from main.log: the identical cyber_policy rejection arrives
// with status 400 on the stream path and 502 through the disconnect channel. Both
// must reach the client, because no credential rotation can satisfy the request.
func TestResponsesWebsocketExposesCyberPolicyRegardlessOfStatus(t *testing.T) {
gin.SetMode(gin.TestMode)
const cyberPolicyBody = `{"error":{"type":"invalid_request","code":"cyber_policy","message":"This content was flagged for possible cybersecurity risk.","param":null}}`
for _, status := range []int{http.StatusBadRequest, http.StatusBadGateway, http.StatusInternalServerError} {
t.Run(strconv.Itoa(status), func(t *testing.T) {
executor := &websocketUpstreamDisconnectExecutor{provider: "codex", subscribed: make(chan string, 1)}
manager := coreauth.NewManager(nil, nil, nil)
manager.RegisterExecutor(executor)
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
var sessionID string
select {
case sessionID = <-executor.subscribed:
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for upstream disconnect subscription")
}
executor.TriggerDisconnect(sessionID, websocketPinnedFailoverStatusError{status: status, msg: cyberPolicyBody})
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
_, payload, errRead := conn.ReadMessage()
if errRead != nil {
t.Fatalf("cyber_policy rejection was hidden at status %d: %v", status, errRead)
}
if got := gjson.GetBytes(payload, "error.code").String(); got != "cyber_policy" {
t.Fatalf("error.code = %q, want cyber_policy: %s", got, payload)
}
})
}
}
func TestResponsesWebsocketTerminalErrorWrittenOnceAcrossForwardAndDisconnect(t *testing.T) {
serverErrCh := make(chan error, 1)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := responsesWebsocketUpgrader.Upgrade(w, r, nil)
if err != nil {
serverErrCh <- err
return
}
writer := newResponsesWebsocketWriter(conn)
cyberPolicyErr := websocketPinnedFailoverStatusError{
status: http.StatusBadRequest,
msg: `{"error":{"type":"invalid_request","code":"cyber_policy","message":"blocked"}}`,
}
errMsg := &interfaces.ErrorMessage{
StatusCode: http.StatusBadRequest,
Error: cyberPolicyErr,
}
start := make(chan struct{})
resultCh := make(chan error, 2)
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
<-start
payload, _, errWrite := writeResponsesWebsocketTerminalError(writer, nil, errMsg, nil)
if !errors.Is(errWrite, websocket.ErrCloseSent) || gjson.GetBytes(payload, "error.code").String() != "cyber_policy" {
resultCh <- fmt.Errorf("err-channel terminal write failed: err=%v payload=%s", errWrite, payload)
}
}()
go func() {
defer wg.Done()
<-start
payload := []byte(`{"type":"error","status":400,"error":{"type":"invalid_request","code":"cyber_policy","message":"blocked"}}`)
writtenPayload, _, errWrite := writeResponsesWebsocketTerminalError(writer, nil, errMsg, payload)
if !errors.Is(errWrite, websocket.ErrCloseSent) || gjson.GetBytes(writtenPayload, "error.code").String() != "cyber_policy" {
resultCh <- fmt.Errorf("payload terminal write failed: err=%v payload=%s", errWrite, writtenPayload)
}
}()
go func() {
defer wg.Done()
<-start
writer.closeForUpstreamDisconnect(errMsg.Error)
}()
close(start)
wg.Wait()
select {
case errResult := <-resultCh:
serverErrCh <- errResult
default:
serverErrCh <- nil
}
}))
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
textFrames := 0
for {
_, payload, errRead := conn.ReadMessage()
if errRead != nil {
break
}
textFrames++
if got := gjson.GetBytes(payload, "error.code").String(); got != "cyber_policy" {
t.Fatalf("terminal error code = %q, want cyber_policy: %s", got, payload)
}
}
if textFrames != 1 {
t.Fatalf("terminal error frame count = %d, want 1", textFrames)
}
if errServer := <-serverErrCh; errServer != nil {
t.Fatalf("server error: %v", errServer)
}
}
func TestResponsesWebsocketCodexWebsocketPassthroughPassesCompactedRequestWithoutTranscriptMerge(t *testing.T) {
gin.SetMode(gin.TestMode)
executor := &websocketDirectCaptureExecutor{done: make(chan struct{})}
manager := coreauth.NewManager(nil, nil, nil)
manager.RegisterExecutor(executor)
auth := &coreauth.Auth{
ID: "auth-ws",
Provider: "codex",
Status: coreauth.StatusActive,
Attributes: map[string]string{"websockets": "true"},
}
if _, err := manager.Register(context.Background(), auth); err != nil {
t.Fatalf("Register auth: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: "test-model"}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth.ID)
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
firstRequest := []byte(`{"type":"response.create","model":"test-model","input":[{"type":"message","role":"user","content":"first"}]}`)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
if errWrite := conn.WriteMessage(websocket.TextMessage, firstRequest); errWrite != nil {
t.Fatalf("write first websocket message: %v", errWrite)
}
if _, _, errRead := conn.ReadMessage(); errRead != nil {
t.Fatalf("read first websocket response: %v", errRead)
}
compactedRequest := []byte(`{"type":"response.create","input":[{"type":"compaction_summary","summary":"compressed history"},{"type":"message","role":"user","content":"after compaction"}]}`)
if errWrite := conn.WriteMessage(websocket.TextMessage, compactedRequest); errWrite != nil {
t.Fatalf("write compacted websocket message: %v", errWrite)
}
if _, _, errRead := conn.ReadMessage(); errRead != nil {
t.Fatalf("read compacted websocket response: %v", errRead)
}
select {
case <-executor.done:
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for websocket passthrough")
}
payloads := executor.Payloads()
if len(payloads) != 2 {
t.Fatalf("passthrough payload count = %d, want 2", len(payloads))
}
if got := gjson.GetBytes(payloads[0], "input").Raw; got != gjson.GetBytes(firstRequest, "input").Raw {
t.Fatalf("first passthrough input = %s, want %s", got, gjson.GetBytes(firstRequest, "input").Raw)
}
if got := gjson.GetBytes(payloads[1], "input").Raw; got != gjson.GetBytes(compactedRequest, "input").Raw {
t.Fatalf("compacted passthrough input = %s, want %s", got, gjson.GetBytes(compactedRequest, "input").Raw)
}
if got := gjson.GetBytes(payloads[1], "model").String(); got != "test-model" {
t.Fatalf("compacted passthrough model = %s, want test-model", got)
}
if bytes.Contains(payloads[1], []byte(`"content":"first"`)) || bytes.Contains(payloads[1], []byte(`"id":"out-1"`)) {
t.Fatalf("compacted passthrough payload contains stale transcript state: %s", payloads[1])
}
authIDs := executor.AuthIDs()
if len(authIDs) != 2 || authIDs[0] != "auth-ws" || authIDs[1] != "auth-ws" {
t.Fatalf("passthrough auth IDs = %v, want [auth-ws auth-ws]", authIDs)
}
}
func TestResponsesWebsocketXAIWebsocketPassthroughKeepsNativeIncrementalRequest(t *testing.T) {
gin.SetMode(gin.TestMode)
modelName := "xai-websocket-passthrough-model"
executor := &websocketDirectCaptureExecutor{provider: "xai", done: make(chan struct{})}
manager := coreauth.NewManager(nil, nil, nil)
manager.RegisterExecutor(executor)
auth := &coreauth.Auth{
ID: "auth-xai-ws",
Provider: "xai",
Status: coreauth.StatusActive,
Attributes: map[string]string{"websockets": "true"},
}
if _, err := manager.Register(context.Background(), auth); err != nil {
t.Fatalf("Register auth: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: modelName}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth.ID)
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
firstRequest := []byte(fmt.Sprintf(`{"type":"response.create","model":%q,"input":[{"type":"message","id":"msg-1","role":"user","content":"first"}]}`, modelName))
if errWrite := conn.WriteMessage(websocket.TextMessage, firstRequest); errWrite != nil {
t.Fatalf("write first websocket message: %v", errWrite)
}
if _, _, errRead := conn.ReadMessage(); errRead != nil {
t.Fatalf("read first websocket response: %v", errRead)
}
secondRequest := []byte(`{"type":"response.create","previous_response_id":"resp-1","input":[{"type":"message","id":"msg-2","role":"user","content":"second"}]}`)
if errWrite := conn.WriteMessage(websocket.TextMessage, secondRequest); errWrite != nil {
t.Fatalf("write second websocket message: %v", errWrite)
}
if _, _, errRead := conn.ReadMessage(); errRead != nil {
t.Fatalf("read second websocket response: %v", errRead)
}
select {
case <-executor.done:
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for websocket passthrough")
}
payloads := executor.Payloads()
if len(payloads) != 2 {
t.Fatalf("xai websocket payload count = %d, want 2", len(payloads))
}
secondPayload := payloads[1]
if got := gjson.GetBytes(secondPayload, "type").String(); got != wsRequestTypeCreate {
t.Fatalf("incremental xai payload type = %q, want %q: %s", got, wsRequestTypeCreate, secondPayload)
}
if got := gjson.GetBytes(secondPayload, "model").String(); got != modelName {
t.Fatalf("second xai payload model = %s, want %s", got, modelName)
}
if got := gjson.GetBytes(secondPayload, "previous_response_id").String(); got != "resp-1" {
t.Fatalf("second xai previous_response_id = %q, want resp-1: %s", got, secondPayload)
}
input := gjson.GetBytes(secondPayload, "input").Array()
if len(input) != 1 || input[0].Get("id").String() != "msg-2" {
t.Fatalf("second xai incremental input is not the client delta: %s", secondPayload)
}
authIDs := executor.AuthIDs()
if len(authIDs) != 2 || authIDs[0] != "auth-xai-ws" || authIDs[1] != "auth-xai-ws" {
t.Fatalf("xai websocket auth IDs = %v, want [auth-xai-ws auth-xai-ws]", authIDs)
}
if got := executor.RequiredUpstreamWebsocketFlags(); len(got) != 2 || got[0] || !got[1] {
t.Fatalf("required upstream websocket flags = %v, want [false true]", got)
}
}
func TestResponsesWebsocketFullRequestCanRouteFromNativeWebsocketToBuiltInProvider(t *testing.T) {
gin.SetMode(gin.TestMode)
const sourceModel = "codex-provider-route-source"
const targetModel = "claude-provider-route-target"
codexExecutor := &websocketDirectCaptureExecutor{provider: "codex"}
claudeExecutor := &websocketDirectCaptureExecutor{provider: "claude"}
manager := coreauth.NewManager(nil, nil, nil)
manager.RegisterExecutor(codexExecutor)
manager.RegisterExecutor(claudeExecutor)
codexAuth := &coreauth.Auth{
ID: "auth-codex-provider-route",
Provider: "codex",
Status: coreauth.StatusActive,
Attributes: map[string]string{"websockets": "true"},
}
claudeAuth := &coreauth.Auth{
ID: "auth-claude-provider-route",
Provider: "claude",
Status: coreauth.StatusActive,
}
for _, auth := range []*coreauth.Auth{codexAuth, claudeAuth} {
if _, err := manager.Register(context.Background(), auth); err != nil {
t.Fatalf("Register auth %s: %v", auth.ID, err)
}
}
registry.GetGlobalRegistry().RegisterClient(codexAuth.ID, codexAuth.Provider, []*registry.ModelInfo{{ID: sourceModel}})
registry.GetGlobalRegistry().RegisterClient(claudeAuth.ID, claudeAuth.Provider, []*registry.ModelInfo{{ID: targetModel}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(codexAuth.ID)
registry.GetGlobalRegistry().UnregisterClient(claudeAuth.ID)
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
base.SetModelRouterHost(&websocketProviderRouteHost{})
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
firstRequest := []byte(fmt.Sprintf(`{"type":"response.create","model":%q,"input":[{"type":"message","id":"msg-1"}]}`, sourceModel))
if errWrite := conn.WriteMessage(websocket.TextMessage, firstRequest); errWrite != nil {
t.Fatalf("write first websocket message: %v", errWrite)
}
if _, _, errRead := conn.ReadMessage(); errRead != nil {
t.Fatalf("read first websocket response: %v", errRead)
}
routedRequest := []byte(fmt.Sprintf(`{"type":"response.create","model":%q,"route_to_claude":true,"input":[{"type":"message","id":"msg-routed"}]}`, sourceModel))
if errWrite := conn.WriteMessage(websocket.TextMessage, routedRequest); errWrite != nil {
t.Fatalf("write routed websocket message: %v", errWrite)
}
_, response, errRead := conn.ReadMessage()
if errRead != nil {
t.Fatalf("read routed websocket response: %v", errRead)
}
if got := gjson.GetBytes(response, "type").String(); got != wsEventTypeCompleted {
t.Fatalf("routed response type = %q, want %q: %s", got, wsEventTypeCompleted, response)
}
if got := len(codexExecutor.Payloads()); got != 1 {
t.Fatalf("codex payload count = %d, want 1", got)
}
claudePayloads := claudeExecutor.Payloads()
if len(claudePayloads) != 1 {
t.Fatalf("claude payload count = %d, want 1", len(claudePayloads))
}
if got := claudeExecutor.Models(); len(got) != 1 || got[0] != targetModel {
t.Fatalf("routed models = %v, want [%s]", got, targetModel)
}
}
func TestResponsesWebsocketHidesProviderRouteAuthFailure(t *testing.T) {
gin.SetMode(gin.TestMode)
const sourceModel = "codex-provider-route-failure-source"
const targetModel = "claude-provider-route-target"
codexExecutor := &websocketDirectCaptureExecutor{provider: "codex"}
claudeExecutor := &websocketDirectCaptureExecutor{provider: "claude", failStatus: http.StatusUnauthorized}
manager := coreauth.NewManager(nil, nil, nil)
manager.RegisterExecutor(codexExecutor)
manager.RegisterExecutor(claudeExecutor)
codexAuth := &coreauth.Auth{ID: "auth-codex-provider-route-failure", Provider: "codex", Status: coreauth.StatusActive, Attributes: map[string]string{"websockets": "true"}}
claudeAuth := &coreauth.Auth{ID: "auth-claude-provider-route-failure", Provider: "claude", Status: coreauth.StatusActive}
for _, auth := range []*coreauth.Auth{codexAuth, claudeAuth} {
if _, err := manager.Register(context.Background(), auth); err != nil {
t.Fatalf("Register auth %s: %v", auth.ID, err)
}
}
registry.GetGlobalRegistry().RegisterClient(codexAuth.ID, codexAuth.Provider, []*registry.ModelInfo{{ID: sourceModel}})
registry.GetGlobalRegistry().RegisterClient(claudeAuth.ID, claudeAuth.Provider, []*registry.ModelInfo{{ID: targetModel}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(codexAuth.ID)
registry.GetGlobalRegistry().UnregisterClient(claudeAuth.ID)
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
base.SetModelRouterHost(&websocketProviderRouteHost{})
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
firstRequest := fmt.Sprintf(`{"type":"response.create","model":%q,"input":[{"type":"message","id":"msg-1"}]}`, sourceModel)
if errWrite := conn.WriteMessage(websocket.TextMessage, []byte(firstRequest)); errWrite != nil {
t.Fatalf("write first request: %v", errWrite)
}
_, firstResponse, errRead := conn.ReadMessage()
if errRead != nil {
t.Fatalf("read first response: %v", errRead)
}
if got := gjson.GetBytes(firstResponse, "type").String(); got != wsEventTypeCompleted {
t.Fatalf("first response type = %q, want %q: %s", got, wsEventTypeCompleted, firstResponse)
}
routedRequest := fmt.Sprintf(`{"type":"response.create","model":%q,"route_to_claude":true,"input":[{"type":"message","id":"msg-routed"}]}`, sourceModel)
if errWrite := conn.WriteMessage(websocket.TextMessage, []byte(routedRequest)); errWrite != nil {
t.Fatalf("write routed request: %v", errWrite)
}
if _, response, errRead := conn.ReadMessage(); errRead == nil {
t.Fatalf("credential error was exposed to the client: %s", response)
}
if got := len(codexExecutor.Payloads()); got != 1 {
t.Fatalf("codex payload count = %d, want 1", got)
}
if got := len(claudeExecutor.Payloads()); got != 1 {
t.Fatalf("claude payload count = %d, want 1", got)
}
}
func TestResponsesWebsocketDeltaRouteToBuiltInProviderRequiresFullReplay(t *testing.T) {
gin.SetMode(gin.TestMode)
const sourceModel = "codex-provider-route-delta-source"
const targetModel = "claude-provider-route-target"
codexExecutor := &websocketDirectCaptureExecutor{provider: "codex"}
claudeExecutor := &websocketDirectCaptureExecutor{provider: "claude"}
manager := coreauth.NewManager(nil, nil, nil)
manager.RegisterExecutor(codexExecutor)
manager.RegisterExecutor(claudeExecutor)
codexAuth := &coreauth.Auth{ID: "auth-codex-provider-route-delta", Provider: "codex", Status: coreauth.StatusActive, Attributes: map[string]string{"websockets": "true"}}
claudeAuth := &coreauth.Auth{ID: "auth-claude-provider-route-delta", Provider: "claude", Status: coreauth.StatusActive}
for _, auth := range []*coreauth.Auth{codexAuth, claudeAuth} {
if _, err := manager.Register(context.Background(), auth); err != nil {
t.Fatalf("Register auth %s: %v", auth.ID, err)
}
}
registry.GetGlobalRegistry().RegisterClient(codexAuth.ID, codexAuth.Provider, []*registry.ModelInfo{{ID: sourceModel}})
registry.GetGlobalRegistry().RegisterClient(claudeAuth.ID, claudeAuth.Provider, []*registry.ModelInfo{{ID: targetModel}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(codexAuth.ID)
registry.GetGlobalRegistry().UnregisterClient(claudeAuth.ID)
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
base.SetModelRouterHost(&websocketProviderRouteHost{})
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
firstRequest := []byte(fmt.Sprintf(`{"type":"response.create","model":%q,"input":[{"type":"message","id":"msg-1"}]}`, sourceModel))
if errWrite := conn.WriteMessage(websocket.TextMessage, firstRequest); errWrite != nil {
t.Fatalf("write first websocket message: %v", errWrite)
}
if _, _, errRead := conn.ReadMessage(); errRead != nil {
t.Fatalf("read first websocket response: %v", errRead)
}
routedDelta := []byte(`{"type":"response.create","route_to_claude":true,"previous_response_id":"resp-1","input":[{"type":"message","id":"msg-routed"}]}`)
if errWrite := conn.WriteMessage(websocket.TextMessage, routedDelta); errWrite != nil {
t.Fatalf("write routed delta: %v", errWrite)
}
_, _, errRead := conn.ReadMessage()
var closeErr *websocket.CloseError
if !errors.As(errRead, &closeErr) {
t.Fatalf("routed delta error = %v, want websocket close", errRead)
}
if closeErr.Code != websocket.CloseServiceRestart || closeErr.Text != wsHTTPReplayRequiredCloseReason {
t.Fatalf("routed delta close = %d %q, want %d %q", closeErr.Code, closeErr.Text, websocket.CloseServiceRestart, wsHTTPReplayRequiredCloseReason)
}
if got := len(codexExecutor.Payloads()); got != 1 {
t.Fatalf("codex payload count = %d, want 1", got)
}
if got := len(claudeExecutor.Payloads()); got != 0 {
t.Fatalf("claude payload count = %d, want 0 before full replay", got)
}
}
func TestResponsesWebsocketClosesForHTTPReplayWhenWebsocketEligibilityChanges(t *testing.T) {
gin.SetMode(gin.TestMode)
modelName := "xai-websocket-mode-change-model"
executor := &websocketDirectCaptureExecutor{provider: "xai", done: make(chan struct{})}
manager := coreauth.NewManager(nil, nil, nil)
manager.RegisterExecutor(executor)
auth := &coreauth.Auth{
ID: "auth-xai-mode-change",
Provider: "xai",
Status: coreauth.StatusActive,
Attributes: map[string]string{"websockets": "true"},
}
if _, err := manager.Register(context.Background(), auth); err != nil {
t.Fatalf("Register auth: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: modelName}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth.ID)
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
firstRequest := []byte(fmt.Sprintf(`{"type":"response.create","model":%q,"input":[{"type":"message","id":"msg-1"}]}`, modelName))
if errWrite := conn.WriteMessage(websocket.TextMessage, firstRequest); errWrite != nil {
t.Fatalf("write first websocket message: %v", errWrite)
}
if _, _, errRead := conn.ReadMessage(); errRead != nil {
t.Fatalf("read first websocket response: %v", errRead)
}
secondRequest := []byte(`{"type":"response.create","previous_response_id":"resp-1","input":[{"type":"message","id":"msg-2"}]}`)
if errWrite := conn.WriteMessage(websocket.TextMessage, secondRequest); errWrite != nil {
t.Fatalf("write second websocket message: %v", errWrite)
}
if _, _, errRead := conn.ReadMessage(); errRead != nil {
t.Fatalf("read second websocket response: %v", errRead)
}
updatedAuth := &coreauth.Auth{
ID: auth.ID,
Provider: auth.Provider,
Status: coreauth.StatusActive,
}
if _, errUpdate := manager.Update(context.Background(), updatedAuth); errUpdate != nil {
t.Fatalf("Update auth: %v", errUpdate)
}
thirdRequest := []byte(`{"type":"response.create","previous_response_id":"resp-2","input":[{"type":"message","id":"msg-3"}]}`)
if errWrite := conn.WriteMessage(websocket.TextMessage, thirdRequest); errWrite != nil {
t.Fatalf("write third websocket message: %v", errWrite)
}
_, _, errRead := conn.ReadMessage()
var closeErr *websocket.CloseError
if !errors.As(errRead, &closeErr) {
t.Fatalf("third response error = %v, want websocket close", errRead)
}
if closeErr.Code != websocket.CloseServiceRestart || closeErr.Text != wsHTTPReplayRequiredCloseReason {
t.Fatalf("third response close = %d %q, want %d %q", closeErr.Code, closeErr.Text, websocket.CloseServiceRestart, wsHTTPReplayRequiredCloseReason)
}
payloads := executor.Payloads()
if len(payloads) != 2 {
t.Fatalf("executor payload count = %d, want 2; transport switch must not call HTTP upstream", len(payloads))
}
second := payloads[1]
if got := gjson.GetBytes(second, "previous_response_id").String(); got != "resp-1" {
t.Fatalf("stable websocket previous_response_id = %q, want resp-1: %s", got, second)
}
if input := gjson.GetBytes(second, "input").Array(); len(input) != 1 || input[0].Get("id").String() != "msg-2" {
t.Fatalf("stable websocket payload is not incremental: %s", second)
}
replayConn, _, errDialReplay := websocket.DefaultDialer.Dial(wsURL, nil)
if errDialReplay != nil {
t.Fatalf("dial replay websocket: %v", errDialReplay)
}
defer func() { _ = replayConn.Close() }()
fullReplay := []byte(fmt.Sprintf(`{"type":"response.create","model":%q,"input":[{"type":"message","id":"msg-1"},{"type":"message","id":"out-1"},{"type":"message","id":"msg-2"},{"type":"message","id":"out-2"},{"type":"message","id":"msg-3"}]}`, modelName))
if errWrite := replayConn.WriteMessage(websocket.TextMessage, fullReplay); errWrite != nil {
t.Fatalf("write full replay: %v", errWrite)
}
if _, _, errReadReplay := replayConn.ReadMessage(); errReadReplay != nil {
t.Fatalf("read full replay response: %v", errReadReplay)
}
deltaAfterReplay := []byte(`{"type":"response.create","previous_response_id":"resp-3","input":[{"type":"message","id":"msg-4"}]}`)
if errWrite := replayConn.WriteMessage(websocket.TextMessage, deltaAfterReplay); errWrite != nil {
t.Fatalf("write delta after replay: %v", errWrite)
}
if _, _, errReadReplay := replayConn.ReadMessage(); errReadReplay != nil {
t.Fatalf("read delta after replay response: %v", errReadReplay)
}
payloads = executor.Payloads()
if len(payloads) != 4 {
t.Fatalf("executor payload count after replay = %d, want 4", len(payloads))
}
httpDelta := payloads[3]
if gjson.GetBytes(httpDelta, "previous_response_id").Exists() {
t.Fatalf("HTTP-mode delta retained previous_response_id: %s", httpDelta)
}
input := gjson.GetBytes(httpDelta, "input").Array()
wantIDs := []string{"msg-1", "out-1", "msg-2", "out-2", "msg-3", "out-3", "msg-4"}
if len(input) != len(wantIDs) {
t.Fatalf("HTTP-mode canonical input len = %d, want %d: %s", len(input), len(wantIDs), httpDelta)
}
for i, wantID := range wantIDs {
if got := input[i].Get("id").String(); got != wantID {
t.Fatalf("HTTP-mode canonical input[%d].id = %q, want %q: %s", i, got, wantID, httpDelta)
}
}
}
func TestResponsesWebsocketRejectsUnknownPreviousResponseOnNewSocket(t *testing.T) {
gin.SetMode(gin.TestMode)
modelName := "xai-websocket-reconnect-model"
executor := &websocketDirectCaptureExecutor{provider: "xai"}
manager := coreauth.NewManager(nil, nil, nil)
manager.RegisterExecutor(executor)
auth := &coreauth.Auth{
ID: "auth-xai-reconnect",
Provider: "xai",
Status: coreauth.StatusActive,
Attributes: map[string]string{"websockets": "true"},
}
if _, err := manager.Register(context.Background(), auth); err != nil {
t.Fatalf("Register auth: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: modelName}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth.ID)
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
request := []byte(fmt.Sprintf(`{"type":"response.create","model":%q,"previous_response_id":"resp-old","input":[{"type":"message","id":"msg-2","role":"user","content":"second"}]}`, modelName))
if errWrite := conn.WriteMessage(websocket.TextMessage, request); errWrite != nil {
t.Fatalf("write websocket message: %v", errWrite)
}
_, payload, errRead := conn.ReadMessage()
if errRead != nil {
t.Fatalf("read websocket response: %v", errRead)
}
if got := gjson.GetBytes(payload, "type").String(); got != wsEventTypeError {
t.Fatalf("response type = %q, want %q: %s", got, wsEventTypeError, payload)
}
if got := int(gjson.GetBytes(payload, "status").Int()); got != http.StatusConflict {
t.Fatalf("response status = %d, want %d: %s", got, http.StatusConflict, payload)
}
if got := gjson.GetBytes(payload, "error.code").String(); got != "previous_response_not_found" {
t.Fatalf("response error code = %q, want previous_response_not_found: %s", got, payload)
}
if got := len(executor.Payloads()); got != 0 {
t.Fatalf("executor payload count = %d, want 0", got)
}
recoveryRequest := []byte(fmt.Sprintf(`{"type":"response.create","model":%q,"input":[{"type":"message","id":"msg-1"},{"type":"message","id":"out-1","role":"assistant"},{"type":"message","id":"msg-2"}]}`, modelName))
if errWrite := conn.WriteMessage(websocket.TextMessage, recoveryRequest); errWrite != nil {
t.Fatalf("write full recovery message: %v", errWrite)
}
_, recoveryPayload, errRead := conn.ReadMessage()
if errRead != nil {
t.Fatalf("read full recovery response: %v", errRead)
}
if got := gjson.GetBytes(recoveryPayload, "type").String(); got != wsEventTypeCompleted {
t.Fatalf("recovery response type = %q, want %q: %s", got, wsEventTypeCompleted, recoveryPayload)
}
payloads := executor.Payloads()
if len(payloads) != 1 {
t.Fatalf("executor payload count after recovery = %d, want 1", len(payloads))
}
if got := len(gjson.GetBytes(payloads[0], "input").Array()); got != 3 {
t.Fatalf("full recovery input len = %d, want 3: %s", got, payloads[0])
}
}
func TestResponsesWebsocketClosesAfterNonRetryableClientError(t *testing.T) {
gin.SetMode(gin.TestMode)
modelName := "xai-websocket-rollback-model"
executor := &websocketCanonicalRollbackExecutor{}
manager := coreauth.NewManager(nil, nil, nil)
manager.RegisterExecutor(executor)
auth := &coreauth.Auth{
ID: "auth-xai-rollback",
Provider: "xai",
Status: coreauth.StatusActive,
}
if _, err := manager.Register(context.Background(), auth); err != nil {
t.Fatalf("Register auth: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: modelName}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth.ID)
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, http.Header{"Session-Id": []string{"rollback-tool-cache-session"}})
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
firstRequest := fmt.Sprintf(`{"type":"response.create","model":%q,"input":[{"type":"message","id":"msg-1"}]}`, modelName)
if errWrite := conn.WriteMessage(websocket.TextMessage, []byte(firstRequest)); errWrite != nil {
t.Fatalf("write first websocket message: %v", errWrite)
}
_, firstResponse, errRead := conn.ReadMessage()
if errRead != nil || gjson.GetBytes(firstResponse, "type").String() != wsEventTypeCompleted {
t.Fatalf("first websocket response = %s, err=%v", firstResponse, errRead)
}
failedRequest := `{"type":"response.create","previous_response_id":"resp-1","input":[{"type":"function_call","id":"fc-failed","call_id":"failed-call","name":"failed_tool","arguments":"{}"},{"type":"function_call_output","id":"fco-failed","call_id":"failed-call","output":"must-not-survive"}]}`
if errWrite := conn.WriteMessage(websocket.TextMessage, []byte(failedRequest)); errWrite != nil {
t.Fatalf("write failed websocket message: %v", errWrite)
}
_, errorResponse, errRead := conn.ReadMessage()
if errRead != nil {
t.Fatalf("read client error response: %v", errRead)
}
if got := gjson.GetBytes(errorResponse, "type").String(); got != wsEventTypeError {
t.Fatalf("client error response type = %q, want %q: %s", got, wsEventTypeError, errorResponse)
}
if got := int(gjson.GetBytes(errorResponse, "status").Int()); got != http.StatusBadRequest {
t.Fatalf("client error response status = %d, want %d: %s", got, http.StatusBadRequest, errorResponse)
}
if _, duplicate, errRead := conn.ReadMessage(); errRead == nil {
t.Fatalf("received frame after terminal client error: %s", duplicate)
}
if got := len(executor.Payloads()); got != 2 {
t.Fatalf("executor payload count = %d, want 2", got)
}
}
// itemNotPersistedUpstreamMessage is the verbatim upstream 404 text raised when a
// turn references a response item the upstream never stored because `store` was
// false. It arrives as plain text, not as a JSON error body.
const itemNotPersistedUpstreamMessage = "Item with id 'rs_0b5f3eb6f51f175c0169ca74e4a85881998539920821603a74' not found. Items are not persisted when `store` is set to false. Try again with `store` set to true, or remove this item from your input."
// TestResponsesWebsocketExposesItemNotPersistedAndRecoversOnReconnect pins the
// store=false item miss end to end. The client must be told (it has to drop the
// stale reference; retrying the same input can never succeed), and the
// conversation must survive: after reconnecting with the full input the turn
// succeeds, and no stale per-socket transcript leaks into the new connection.
func TestResponsesWebsocketExposesItemNotPersistedAndRecoversOnReconnect(t *testing.T) {
gin.SetMode(gin.TestMode)
modelName := "xai-item-miss-model"
executor := &websocketCanonicalRollbackExecutor{
failErr: websocketPinnedFailoverStatusError{
status: http.StatusNotFound,
msg: itemNotPersistedUpstreamMessage,
},
}
manager := coreauth.NewManager(nil, nil, nil)
manager.RegisterExecutor(executor)
auth := &coreauth.Auth{ID: "auth-xai-item-miss", Provider: "xai", Status: coreauth.StatusActive}
if _, err := manager.Register(context.Background(), auth); err != nil {
t.Fatalf("Register auth: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: modelName}})
t.Cleanup(func() { registry.GetGlobalRegistry().UnregisterClient(auth.ID) })
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
sessionHeader := http.Header{"Session-Id": []string{"item-miss-session"}}
conn, _, err := websocket.DefaultDialer.Dial(wsURL, sessionHeader)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
firstRequest := fmt.Sprintf(`{"type":"response.create","model":%q,"input":[{"type":"message","id":"msg-1"}]}`, modelName)
if errWrite := conn.WriteMessage(websocket.TextMessage, []byte(firstRequest)); errWrite != nil {
t.Fatalf("write first request: %v", errWrite)
}
if _, firstResponse, errRead := conn.ReadMessage(); errRead != nil ||
gjson.GetBytes(firstResponse, "type").String() != wsEventTypeCompleted {
t.Fatalf("first response = %s, err=%v", firstResponse, errRead)
}
// The turn references a reasoning item the upstream no longer holds.
staleRequest := `{"type":"response.create","previous_response_id":"resp-1","input":[{"type":"reasoning","id":"rs_0b5f3eb6f51f175c0169ca74e4a85881998539920821603a74"},{"type":"message","id":"msg-2"}]}`
if errWrite := conn.WriteMessage(websocket.TextMessage, []byte(staleRequest)); errWrite != nil {
t.Fatalf("write stale request: %v", errWrite)
}
_, errorResponse, errRead := conn.ReadMessage()
if errRead != nil {
t.Fatalf("item miss was hidden from the client: %v", errRead)
}
if got := gjson.GetBytes(errorResponse, "type").String(); got != wsEventTypeError {
t.Fatalf("response type = %q, want %q: %s", got, wsEventTypeError, errorResponse)
}
if got := int(gjson.GetBytes(errorResponse, "status").Int()); got != http.StatusNotFound {
t.Fatalf("status = %d, want %d: %s", got, http.StatusNotFound, errorResponse)
}
if msg := gjson.GetBytes(errorResponse, "error.message").String(); !strings.Contains(msg, "Items are not persisted") {
t.Fatalf("error.message lost the upstream reason: %q", msg)
}
if _, extra, errRead := conn.ReadMessage(); errRead == nil {
t.Fatalf("received frame after terminal error: %s", extra)
}
// The client rebuilds the conversation on a new socket with the full input.
reconn, _, errDial := websocket.DefaultDialer.Dial(wsURL, sessionHeader)
if errDial != nil {
t.Fatalf("reconnect websocket: %v", errDial)
}
defer func() { _ = reconn.Close() }()
fullRequest := fmt.Sprintf(`{"type":"response.create","model":%q,"input":[{"type":"message","id":"msg-1"},{"type":"message","id":"msg-2"}]}`, modelName)
if errWrite := reconn.WriteMessage(websocket.TextMessage, []byte(fullRequest)); errWrite != nil {
t.Fatalf("write rebuilt request: %v", errWrite)
}
_, recovered, errRead := reconn.ReadMessage()
if errRead != nil {
t.Fatalf("read rebuilt response: %v", errRead)
}
if got := gjson.GetBytes(recovered, "type").String(); got != wsEventTypeCompleted {
t.Fatalf("rebuilt response type = %q, want %q: %s", got, wsEventTypeCompleted, recovered)
}
payloads := executor.Payloads()
if len(payloads) != 3 {
t.Fatalf("upstream payload count = %d, want 3", len(payloads))
}
// The rebuilt turn must carry the full input and none of the failed turn's state.
rebuilt := payloads[2]
if got := gjson.GetBytes(rebuilt, "previous_response_id").String(); got != "" {
t.Fatalf("rebuilt upstream request still pinned previous_response_id=%q: %s", got, rebuilt)
}
inputIDs := gjson.GetBytes(rebuilt, "input.#.id").Array()
if len(inputIDs) != 2 || inputIDs[0].String() != "msg-1" || inputIDs[1].String() != "msg-2" {
t.Fatalf("rebuilt upstream input lost context: %s", rebuilt)
}
if strings.Contains(string(rebuilt), "rs_0b5f3eb6f51f175c0169ca74e4a85881998539920821603a74") {
t.Fatalf("rebuilt upstream request replayed the stale item: %s", rebuilt)
}
}
func TestResponsesWebsocketSwitchesPinnedAuthAcrossProviders(t *testing.T) {
for _, testCase := range []struct {
name string
xaiWebsockets bool
returnToDifferentXAIModel bool
}{
{name: "xai SSE", xaiWebsockets: false},
{name: "xai websocket", xaiWebsockets: true},
{name: "xai websocket different model", xaiWebsockets: true, returnToDifferentXAIModel: true},
} {
t.Run(testCase.name, func(t *testing.T) {
gin.SetMode(gin.TestMode)
xaiModel := "xai-provider-switch-" + strings.ReplaceAll(testCase.name, " ", "-")
returnXAIModel := xaiModel
if testCase.returnToDifferentXAIModel {
returnXAIModel += "-return"
}
codexModel := "codex-provider-switch-" + strings.ReplaceAll(testCase.name, " ", "-")
xaiExecutor := &websocketDirectCaptureExecutor{provider: "xai"}
codexExecutor := &websocketDirectCaptureExecutor{provider: "codex"}
xaiAuth := &coreauth.Auth{
ID: "auth-" + xaiModel,
Provider: "xai",
Status: coreauth.StatusActive,
}
if testCase.xaiWebsockets {
xaiAuth.Attributes = map[string]string{"websockets": "true"}
}
codexAuth := &coreauth.Auth{
ID: "auth-" + codexModel,
Provider: "codex",
Status: coreauth.StatusActive,
Attributes: map[string]string{"websockets": "true"},
}
selector := &orderedWebsocketSelector{order: []string{xaiAuth.ID, codexAuth.ID}}
manager := coreauth.NewManager(nil, selector, nil)
manager.RegisterExecutor(xaiExecutor)
manager.RegisterExecutor(codexExecutor)
if _, errRegister := manager.Register(context.Background(), xaiAuth); errRegister != nil {
t.Fatalf("Register xAI auth: %v", errRegister)
}
if _, errRegister := manager.Register(context.Background(), codexAuth); errRegister != nil {
t.Fatalf("Register Codex auth: %v", errRegister)
}
registry.GetGlobalRegistry().RegisterClient(xaiAuth.ID, xaiAuth.Provider, []*registry.ModelInfo{{ID: xaiModel}})
registry.GetGlobalRegistry().RegisterClient(codexAuth.ID, codexAuth.Provider, []*registry.ModelInfo{{ID: codexModel}})
registeredAuthIDs := []string{xaiAuth.ID, codexAuth.ID}
if testCase.xaiWebsockets {
xaiAlternateAuth := &coreauth.Auth{
ID: "auth-alternate-" + xaiModel,
Provider: "xai",
Status: coreauth.StatusActive,
Attributes: map[string]string{"websockets": "true"},
}
selector.order = append(selector.order, xaiAlternateAuth.ID)
if _, errRegister := manager.Register(context.Background(), xaiAlternateAuth); errRegister != nil {
t.Fatalf("Register alternate xAI auth: %v", errRegister)
}
alternateModels := []*registry.ModelInfo{{ID: xaiModel}}
if testCase.returnToDifferentXAIModel {
alternateModels = []*registry.ModelInfo{{ID: returnXAIModel}}
}
registry.GetGlobalRegistry().RegisterClient(xaiAlternateAuth.ID, xaiAlternateAuth.Provider, alternateModels)
registeredAuthIDs = append(registeredAuthIDs, xaiAlternateAuth.ID)
}
t.Cleanup(func() {
for _, authID := range registeredAuthIDs {
registry.GetGlobalRegistry().UnregisterClient(authID)
}
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, errDial := websocket.DefaultDialer.Dial(wsURL, nil)
if errDial != nil {
t.Fatalf("dial websocket: %v", errDial)
}
defer func() {
if errClose := conn.Close(); errClose != nil {
t.Errorf("close websocket: %v", errClose)
}
}()
requests := []string{
fmt.Sprintf(`{"type":"response.create","model":%q,"input":[{"type":"message","id":"msg-xai-1"}]}`, xaiModel),
fmt.Sprintf(`{"type":"response.create","model":%q,"input":[{"type":"message","id":"msg-codex-1"}]}`, codexModel),
fmt.Sprintf(`{"type":"response.create","model":%q,"input":[{"type":"message","id":"msg-xai-2"}]}`, returnXAIModel),
`{"type":"response.create","input":[{"type":"message","id":"msg-xai-3"}]}`,
}
for index, request := range requests {
turn := index + 1
if errWrite := conn.WriteMessage(websocket.TextMessage, []byte(request)); errWrite != nil {
t.Fatalf("write websocket message %d: %v", turn, errWrite)
}
_, payload, errRead := conn.ReadMessage()
if errRead != nil {
t.Fatalf("read websocket response %d: %v", turn, errRead)
}
if got := gjson.GetBytes(payload, "type").String(); got != wsEventTypeCompleted {
t.Fatalf("response %d type = %s, want %s: %s", turn, got, wsEventTypeCompleted, payload)
}
}
wantReturnAuthID := xaiAuth.ID
if testCase.returnToDifferentXAIModel {
wantReturnAuthID = "auth-alternate-" + xaiModel
}
if got := xaiExecutor.AuthIDs(); len(got) != 3 || got[0] != xaiAuth.ID || got[1] != wantReturnAuthID || got[2] != wantReturnAuthID {
t.Fatalf("xAI auth IDs = %v, want [%s %s %s]", got, xaiAuth.ID, wantReturnAuthID, wantReturnAuthID)
}
if got := codexExecutor.AuthIDs(); len(got) != 1 || got[0] != codexAuth.ID {
t.Fatalf("Codex auth IDs = %v, want [%s]", got, codexAuth.ID)
}
})
}
}
func TestResponsesWebsocketPinnedAuthMatchesModel(t *testing.T) {
modelA := "xai-pinned-auth-model-a"
modelB := "xai-pinned-auth-model-b"
auth := &coreauth.Auth{ID: "xai-pinned-auth", Provider: "xai", Status: coreauth.StatusActive}
otherAuthID := "xai-pinned-auth-other"
registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: modelA}})
registry.GetGlobalRegistry().RegisterClient(otherAuthID, auth.Provider, []*registry.ModelInfo{{ID: modelB}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth.ID)
registry.GetGlobalRegistry().UnregisterClient(otherAuthID)
})
if !responsesWebsocketPinnedAuthMatchesModel(auth, modelA, modelA, false) {
t.Fatal("expected registered auth to match its supported model")
}
if responsesWebsocketPinnedAuthMatchesModel(auth, modelB, modelA, false) {
t.Fatal("registered auth matched an unsupported model from the same provider")
}
disabledAuth := auth.Clone()
disabledAuth.Disabled = true
if responsesWebsocketPinnedAuthMatchesModel(disabledAuth, modelA, modelA, false) {
t.Fatal("disabled auth matched a model")
}
cooldownAuth := auth.Clone()
cooldownAuth.ModelStates = map[string]*coreauth.ModelState{
modelA: {Unavailable: true, NextRetryAfter: time.Now().Add(time.Minute)},
}
if responsesWebsocketPinnedAuthMatchesModel(cooldownAuth, modelA, modelA, false) {
t.Fatal("auth in model cooldown matched a model")
}
unregisteredAuth := &coreauth.Auth{ID: "unregistered-auth", Provider: "xai", Status: coreauth.StatusActive}
if responsesWebsocketPinnedAuthMatchesModel(unregisteredAuth, modelA, modelA, false) {
t.Fatal("unregistered ordinary auth matched a model")
}
if !responsesWebsocketPinnedAuthMatchesModel(unregisteredAuth, modelA, modelA, true) {
t.Fatal("expected Home runtime auth to match its pinned model")
}
if responsesWebsocketPinnedAuthMatchesModel(unregisteredAuth, modelB, modelA, true) {
t.Fatal("Home runtime auth matched a different model")
}
}
func TestWebsocketUpstreamSupportsIncrementalInputForModel(t *testing.T) {
manager := coreauth.NewManager(nil, nil, nil)
auth := &coreauth.Auth{
ID: "auth-ws",
Provider: "test-provider",
Status: coreauth.StatusActive,
Attributes: map[string]string{"websockets": "true"},
}
if _, err := manager.Register(context.Background(), auth); err != nil {
t.Fatalf("Register auth: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: "test-model"}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth.ID)
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
if !h.websocketUpstreamSupportsIncrementalInputForModel("test-model") {
t.Fatalf("expected websocket-capable upstream for test-model")
}
}
func TestWebsocketUpstreamSupportsIncrementalInputForXAI(t *testing.T) {
manager := coreauth.NewManager(nil, nil, nil)
auth := &coreauth.Auth{
ID: "auth-xai-ws",
Provider: "xai",
Status: coreauth.StatusActive,
Attributes: map[string]string{"websockets": "true"},
}
if _, err := manager.Register(context.Background(), auth); err != nil {
t.Fatalf("Register auth: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: "xai-test-model"}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth.ID)
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
if !h.websocketUpstreamSupportsIncrementalInputForModel("xai-test-model") {
t.Fatalf("expected xai websocket upstream to support previous_response_id incremental input")
}
}
func TestResponsesWebsocketUsesUpstreamWebsocketPassthroughForXAI(t *testing.T) {
manager := coreauth.NewManager(nil, nil, nil)
executor := &websocketProviderCaptureExecutor{provider: "xai"}
manager.RegisterExecutor(executor)
modelName := "xai-passthrough-model"
auth := &coreauth.Auth{
ID: "auth-xai-ws",
Provider: "xai",
Status: coreauth.StatusActive,
Attributes: map[string]string{"websockets": "true"},
}
if _, err := manager.Register(context.Background(), auth); err != nil {
t.Fatalf("Register auth: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: modelName}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth.ID)
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
if !h.responsesWebsocketUsesUpstreamWebsocketPassthrough(modelName) {
t.Fatalf("expected xai websocket upstream passthrough for %s", modelName)
}
}
func TestWebsocketUpstreamSupportsCompactionReplayForModel(t *testing.T) {
manager := coreauth.NewManager(nil, nil, nil)
auth := &coreauth.Auth{
ID: "auth-codex",
Provider: "codex",
Status: coreauth.StatusActive,
}
if _, err := manager.Register(context.Background(), auth); err != nil {
t.Fatalf("Register auth: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: "test-model"}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth.ID)
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
if !h.websocketUpstreamSupportsCompactionReplayForModel("test-model") {
t.Fatalf("expected codex upstream to support compaction replay")
}
}
func TestWebsocketUpstreamSupportsCompactionReplayForModelFalseWhenMixedBackends(t *testing.T) {
manager := coreauth.NewManager(nil, nil, nil)
auths := []*coreauth.Auth{
{ID: "auth-codex", Provider: "codex", Status: coreauth.StatusActive},
{ID: "auth-claude", Provider: "claude", Status: coreauth.StatusActive},
}
for _, auth := range auths {
if _, err := manager.Register(context.Background(), auth); err != nil {
t.Fatalf("Register auth %s: %v", auth.ID, err)
}
registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: "test-model"}})
}
t.Cleanup(func() {
for _, auth := range auths {
registry.GetGlobalRegistry().UnregisterClient(auth.ID)
}
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
if h.websocketUpstreamSupportsCompactionReplayForModel("test-model") {
t.Fatalf("expected mixed backend model to disable compaction replay bypass")
}
}
func TestResponsesWebsocketPrewarmHandledLocallyForSSEUpstream(t *testing.T) {
gin.SetMode(gin.TestMode)
executor := &websocketCaptureExecutor{}
manager := coreauth.NewManager(nil, nil, nil)
manager.RegisterExecutor(executor)
auth := &coreauth.Auth{ID: "auth-sse", Provider: executor.Identifier(), Status: coreauth.StatusActive}
if _, err := manager.Register(context.Background(), auth); err != nil {
t.Fatalf("Register auth: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: "test-model"}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth.ID)
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() {
errClose := conn.Close()
if errClose != nil {
t.Fatalf("close websocket: %v", errClose)
}
}()
errWrite := conn.WriteMessage(websocket.TextMessage, []byte(`{"type":"response.create","model":"test-model","generate":false}`))
if errWrite != nil {
t.Fatalf("write prewarm websocket message: %v", errWrite)
}
_, createdPayload, errReadMessage := conn.ReadMessage()
if errReadMessage != nil {
t.Fatalf("read prewarm created message: %v", errReadMessage)
}
if gjson.GetBytes(createdPayload, "type").String() != "response.created" {
t.Fatalf("created payload type = %s, want response.created", gjson.GetBytes(createdPayload, "type").String())
}
prewarmResponseID := gjson.GetBytes(createdPayload, "response.id").String()
if prewarmResponseID == "" {
t.Fatalf("prewarm response id is empty")
}
if got := gjson.GetBytes(createdPayload, "response.model").String(); got != "test-model" {
t.Fatalf("prewarm response.model = %q, want test-model", got)
}
if executor.streamCalls != 0 {
t.Fatalf("stream calls after prewarm = %d, want 0", executor.streamCalls)
}
_, completedPayload, errReadMessage := conn.ReadMessage()
if errReadMessage != nil {
t.Fatalf("read prewarm completed message: %v", errReadMessage)
}
if gjson.GetBytes(completedPayload, "type").String() != wsEventTypeCompleted {
t.Fatalf("completed payload type = %s, want %s", gjson.GetBytes(completedPayload, "type").String(), wsEventTypeCompleted)
}
if gjson.GetBytes(completedPayload, "response.id").String() != prewarmResponseID {
t.Fatalf("completed response id = %s, want %s", gjson.GetBytes(completedPayload, "response.id").String(), prewarmResponseID)
}
if gjson.GetBytes(completedPayload, "response.usage.total_tokens").Int() != 0 {
t.Fatalf("prewarm total tokens = %d, want 0", gjson.GetBytes(completedPayload, "response.usage.total_tokens").Int())
}
secondRequest := fmt.Sprintf(`{"type":"response.create","previous_response_id":%q,"input":[{"type":"message","id":"msg-1"}]}`, prewarmResponseID)
errWrite = conn.WriteMessage(websocket.TextMessage, []byte(secondRequest))
if errWrite != nil {
t.Fatalf("write follow-up websocket message: %v", errWrite)
}
_, upstreamPayload, errReadMessage := conn.ReadMessage()
if errReadMessage != nil {
t.Fatalf("read upstream completed message: %v", errReadMessage)
}
if gjson.GetBytes(upstreamPayload, "type").String() != wsEventTypeCompleted {
t.Fatalf("upstream payload type = %s, want %s", gjson.GetBytes(upstreamPayload, "type").String(), wsEventTypeCompleted)
}
if executor.streamCalls != 1 {
t.Fatalf("stream calls after follow-up = %d, want 1", executor.streamCalls)
}
if len(executor.payloads) != 1 {
t.Fatalf("captured upstream payloads = %d, want 1", len(executor.payloads))
}
forwarded := executor.payloads[0]
if gjson.GetBytes(forwarded, "previous_response_id").Exists() {
t.Fatalf("previous_response_id leaked upstream: %s", forwarded)
}
if gjson.GetBytes(forwarded, "generate").Exists() {
t.Fatalf("generate leaked upstream: %s", forwarded)
}
if gjson.GetBytes(forwarded, "model").String() != "test-model" {
t.Fatalf("forwarded model = %s, want test-model", gjson.GetBytes(forwarded, "model").String())
}
input := gjson.GetBytes(forwarded, "input").Array()
if len(input) != 1 || input[0].Get("id").String() != "msg-1" {
t.Fatalf("unexpected forwarded input: %s", forwarded)
}
}
func TestResponsesWebsocketMergesTranscriptForNonPassthroughUpstream(t *testing.T) {
gin.SetMode(gin.TestMode)
executor := &websocketCaptureExecutor{}
manager := coreauth.NewManager(nil, nil, nil)
manager.RegisterExecutor(executor)
auth := &coreauth.Auth{
ID: "auth-ws",
Provider: executor.Identifier(),
Status: coreauth.StatusActive,
Attributes: map[string]string{"websockets": "true"},
}
if _, err := manager.Register(context.Background(), auth); err != nil {
t.Fatalf("Register auth: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: "test-model"}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth.ID)
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() {
if errClose := conn.Close(); errClose != nil {
t.Fatalf("close websocket: %v", errClose)
}
}()
requests := []string{
`{"type":"response.create","model":"test-model","input":[{"type":"message","id":"msg-1"}]}`,
`{"type":"response.create","input":[{"type":"message","id":"msg-2"}]}`,
}
for i := range requests {
if errWrite := conn.WriteMessage(websocket.TextMessage, []byte(requests[i])); errWrite != nil {
t.Fatalf("write websocket message %d: %v", i+1, errWrite)
}
_, payload, errReadMessage := conn.ReadMessage()
if errReadMessage != nil {
t.Fatalf("read websocket message %d: %v", i+1, errReadMessage)
}
if got := gjson.GetBytes(payload, "type").String(); got != wsEventTypeCompleted {
t.Fatalf("message %d payload type = %s, want %s", i+1, got, wsEventTypeCompleted)
}
}
if len(executor.payloads) != 2 {
t.Fatalf("upstream payload count = %d, want 2", len(executor.payloads))
}
secondPayload := executor.payloads[1]
if gjson.GetBytes(secondPayload, "previous_response_id").Exists() {
t.Fatalf("previous_response_id must not be sent on non-passthrough upstream: %s", secondPayload)
}
input := gjson.GetBytes(secondPayload, "input").Array()
if len(input) != 3 {
t.Fatalf("second upstream input len = %d, want 3: %s", len(input), secondPayload)
}
if input[0].Get("id").String() != "msg-1" || input[1].Get("id").String() != "out-1" || input[2].Get("id").String() != "msg-2" {
t.Fatalf("unexpected merged upstream input: %s", secondPayload)
}
}
func TestResponsesWebsocketDoesNotInjectPreviousResponseIDWhenPendingToolOutputMissing(t *testing.T) {
gin.SetMode(gin.TestMode)
executor := &websocketCompactionCaptureExecutor{}
manager := coreauth.NewManager(nil, nil, nil)
manager.RegisterExecutor(executor)
auth := &coreauth.Auth{
ID: "auth-ws",
Provider: executor.Identifier(),
Status: coreauth.StatusActive,
Attributes: map[string]string{"websockets": "true"},
}
if _, err := manager.Register(context.Background(), auth); err != nil {
t.Fatalf("Register auth: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: "test-model"}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth.ID)
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() {
if errClose := conn.Close(); errClose != nil {
t.Fatalf("close websocket: %v", errClose)
}
}()
requests := []string{
`{"type":"response.create","model":"test-model","input":[{"type":"message","id":"msg-1"}]}`,
`{"type":"response.create","input":[{"type":"message","role":"user","id":"summary-1","content":"compacted summary"}]}`,
}
for i := range requests {
if errWrite := conn.WriteMessage(websocket.TextMessage, []byte(requests[i])); errWrite != nil {
t.Fatalf("write websocket message %d: %v", i+1, errWrite)
}
_, payload, errReadMessage := conn.ReadMessage()
if errReadMessage != nil {
t.Fatalf("read websocket message %d: %v", i+1, errReadMessage)
}
if got := gjson.GetBytes(payload, "type").String(); got != wsEventTypeCompleted {
t.Fatalf("message %d payload type = %s, want %s", i+1, got, wsEventTypeCompleted)
}
}
executor.mu.Lock()
payloads := append([][]byte(nil), executor.streamPayloads...)
executor.mu.Unlock()
if len(payloads) != 2 {
t.Fatalf("upstream payload count = %d, want 2", len(payloads))
}
secondPayload := payloads[1]
if gjson.GetBytes(secondPayload, "previous_response_id").Exists() {
t.Fatalf("previous_response_id must not be injected when pending tool output is missing: %s", secondPayload)
}
input := gjson.GetBytes(secondPayload, "input").Array()
if len(input) != 3 {
t.Fatalf("second upstream input len = %d, want 3: %s", len(input), secondPayload)
}
if input[0].Get("id").String() != "msg-1" || input[1].Get("id").String() != "fc-1" || input[2].Get("id").String() != "summary-1" {
t.Fatalf("unexpected merged upstream input when pending tool output is missing: %s", secondPayload)
}
}
func TestResponsesWebsocketStripsGenerateWhenWebsocketAttemptFallsBackToHTTP(t *testing.T) {
gin.SetMode(gin.TestMode)
selector := &orderedWebsocketSelector{order: []string{"auth-ws", "auth-http", "auth-http"}}
executor := &websocketBootstrapFallbackExecutor{}
manager := coreauth.NewManager(nil, selector, nil)
manager.RegisterExecutor(executor)
authWS := &coreauth.Auth{
ID: "auth-ws",
Provider: executor.Identifier(),
Status: coreauth.StatusActive,
Attributes: map[string]string{"websockets": "true"},
}
if _, err := manager.Register(context.Background(), authWS); err != nil {
t.Fatalf("Register websocket auth: %v", err)
}
authHTTP := &coreauth.Auth{ID: "auth-http", Provider: executor.Identifier(), Status: coreauth.StatusActive}
if _, err := manager.Register(context.Background(), authHTTP); err != nil {
t.Fatalf("Register HTTP auth: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(authWS.ID, authWS.Provider, []*registry.ModelInfo{{ID: "test-model"}})
registry.GetGlobalRegistry().RegisterClient(authHTTP.ID, authHTTP.Provider, []*registry.ModelInfo{{ID: "test-model"}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(authWS.ID)
registry.GetGlobalRegistry().UnregisterClient(authHTTP.ID)
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() {
if errClose := conn.Close(); errClose != nil {
t.Fatalf("close websocket: %v", errClose)
}
}()
request := `{"type":"response.create","model":"test-model","generate":true,"input":[{"type":"message","id":"msg-1"}]}`
if errWrite := conn.WriteMessage(websocket.TextMessage, []byte(request)); errWrite != nil {
t.Fatalf("write websocket message: %v", errWrite)
}
_, payload, errReadMessage := conn.ReadMessage()
if errReadMessage != nil {
t.Fatalf("read websocket message: %v", errReadMessage)
}
if got := gjson.GetBytes(payload, "type").String(); got != wsEventTypeCompleted {
t.Fatalf("payload type = %s, want %s: %s", got, wsEventTypeCompleted, payload)
}
if got := executor.AuthIDs(); len(got) != 2 || got[0] != "auth-ws" || got[1] != "auth-http" {
t.Fatalf("selected auth IDs = %v, want [auth-ws auth-http]", got)
}
wsPayloads := executor.Payloads("auth-ws")
if len(wsPayloads) != 1 {
t.Fatalf("auth-ws payload count = %d, want 1", len(wsPayloads))
}
if !gjson.GetBytes(wsPayloads[0], "generate").Exists() {
t.Fatalf("websocket attempt payload unexpectedly stripped generate: %s", wsPayloads[0])
}
httpPayloads := executor.Payloads("auth-http")
if len(httpPayloads) != 1 {
t.Fatalf("auth-http payload count = %d, want 1", len(httpPayloads))
}
if gjson.GetBytes(httpPayloads[0], "generate").Exists() {
t.Fatalf("generate leaked after HTTP fallback: %s", httpPayloads[0])
}
secondRequest := `{"type":"response.create","previous_response_id":"resp-http","input":[{"type":"message","id":"msg-2"}]}`
if errWrite := conn.WriteMessage(websocket.TextMessage, []byte(secondRequest)); errWrite != nil {
t.Fatalf("write second websocket message: %v", errWrite)
}
_, secondPayload, errReadSecond := conn.ReadMessage()
if errReadSecond != nil {
t.Fatalf("read second websocket message: %v", errReadSecond)
}
if got := gjson.GetBytes(secondPayload, "type").String(); got != wsEventTypeCompleted {
t.Fatalf("second payload type = %s, want %s: %s", got, wsEventTypeCompleted, secondPayload)
}
if got := executor.AuthIDs(); len(got) != 3 || got[2] != "auth-http" {
t.Fatalf("selected auth IDs after HTTP retry = %v, want [auth-ws auth-http auth-http]", got)
}
}
func TestWebsocketClientAddressUsesGinClientIP(t *testing.T) {
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
c, engine := gin.CreateTestContext(recorder)
if err := engine.SetTrustedProxies([]string{"0.0.0.0/0", "::/0"}); err != nil {
t.Fatalf("SetTrustedProxies: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/v1/responses/ws", nil)
req.RemoteAddr = "172.18.0.1:34282"
req.Header.Set("X-Forwarded-For", "203.0.113.7")
c.Request = req
if got := websocketClientAddress(c); got != strings.TrimSpace(c.ClientIP()) {
t.Fatalf("websocketClientAddress = %q, ClientIP = %q", got, c.ClientIP())
}
}
func TestWebsocketClientAddressReturnsEmptyForNilContext(t *testing.T) {
if got := websocketClientAddress(nil); got != "" {
t.Fatalf("websocketClientAddress(nil) = %q, want empty", got)
}
}
func TestResponsesWebsocketPinsOnlyWebsocketCapableAuth(t *testing.T) {
gin.SetMode(gin.TestMode)
selector := &orderedWebsocketSelector{order: []string{"auth-sse", "auth-ws"}}
executor := &websocketAuthCaptureExecutor{}
manager := coreauth.NewManager(nil, selector, nil)
manager.RegisterExecutor(executor)
authSSE := &coreauth.Auth{ID: "auth-sse", Provider: executor.Identifier(), Status: coreauth.StatusActive}
if _, err := manager.Register(context.Background(), authSSE); err != nil {
t.Fatalf("Register SSE auth: %v", err)
}
authWS := &coreauth.Auth{
ID: "auth-ws",
Provider: executor.Identifier(),
Status: coreauth.StatusActive,
Attributes: map[string]string{"websockets": "true"},
}
if _, err := manager.Register(context.Background(), authWS); err != nil {
t.Fatalf("Register websocket auth: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(authSSE.ID, authSSE.Provider, []*registry.ModelInfo{{ID: "test-model"}})
registry.GetGlobalRegistry().RegisterClient(authWS.ID, authWS.Provider, []*registry.ModelInfo{{ID: "test-model"}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(authSSE.ID)
registry.GetGlobalRegistry().UnregisterClient(authWS.ID)
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() {
if errClose := conn.Close(); errClose != nil {
t.Fatalf("close websocket: %v", errClose)
}
}()
requests := []string{
`{"type":"response.create","model":"test-model","input":[{"type":"message","id":"msg-1"}]}`,
`{"type":"response.create","input":[{"type":"message","id":"msg-2"}]}`,
}
for i := range requests {
if errWrite := conn.WriteMessage(websocket.TextMessage, []byte(requests[i])); errWrite != nil {
t.Fatalf("write websocket message %d: %v", i+1, errWrite)
}
_, payload, errReadMessage := conn.ReadMessage()
if errReadMessage != nil {
t.Fatalf("read websocket message %d: %v", i+1, errReadMessage)
}
if got := gjson.GetBytes(payload, "type").String(); got != wsEventTypeCompleted {
t.Fatalf("message %d payload type = %s, want %s", i+1, got, wsEventTypeCompleted)
}
}
if got := executor.AuthIDs(); len(got) != 2 || got[0] != "auth-sse" || got[1] != "auth-ws" {
t.Fatalf("selected auth IDs = %v, want [auth-sse auth-ws]", got)
}
}
func TestResponsesWebsocketUsesNativeIncrementalAfterPinningWebsocketAuthFromMixedPool(t *testing.T) {
gin.SetMode(gin.TestMode)
modelName := "xai-mixed-pool-model"
selector := &orderedWebsocketSelector{order: []string{"auth-http", "auth-ws"}}
executor := &websocketDirectCaptureExecutor{provider: "xai"}
manager := coreauth.NewManager(nil, selector, nil)
manager.RegisterExecutor(executor)
authHTTP := &coreauth.Auth{ID: "auth-http", Provider: "xai", Status: coreauth.StatusActive}
if _, err := manager.Register(context.Background(), authHTTP); err != nil {
t.Fatalf("Register HTTP auth: %v", err)
}
authWS := &coreauth.Auth{
ID: "auth-ws",
Provider: "xai",
Status: coreauth.StatusActive,
Attributes: map[string]string{"websockets": "true"},
}
if _, err := manager.Register(context.Background(), authWS); err != nil {
t.Fatalf("Register websocket auth: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(authHTTP.ID, authHTTP.Provider, []*registry.ModelInfo{{ID: modelName}})
registry.GetGlobalRegistry().RegisterClient(authWS.ID, authWS.Provider, []*registry.ModelInfo{{ID: modelName}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(authHTTP.ID)
registry.GetGlobalRegistry().UnregisterClient(authWS.ID)
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
requests := []string{
fmt.Sprintf(`{"type":"response.create","model":%q,"input":[{"type":"message","id":"msg-1"}]}`, modelName),
`{"type":"response.create","previous_response_id":"resp-1","input":[{"type":"message","id":"msg-2"}]}`,
`{"type":"response.create","previous_response_id":"resp-2","input":[{"type":"message","id":"msg-3"}]}`,
}
for i := range requests {
if errWrite := conn.WriteMessage(websocket.TextMessage, []byte(requests[i])); errWrite != nil {
t.Fatalf("write websocket message %d: %v", i+1, errWrite)
}
if _, _, errRead := conn.ReadMessage(); errRead != nil {
t.Fatalf("read websocket response %d: %v", i+1, errRead)
}
}
if got := executor.AuthIDs(); len(got) != 3 || got[0] != "auth-http" || got[1] != "auth-ws" || got[2] != "auth-ws" {
t.Fatalf("selected auth IDs = %v, want [auth-http auth-ws auth-ws]", got)
}
payloads := executor.Payloads()
if len(payloads) != 3 {
t.Fatalf("payload count = %d, want 3", len(payloads))
}
if gjson.GetBytes(payloads[1], "previous_response_id").Exists() || len(gjson.GetBytes(payloads[1], "input").Array()) != 3 {
t.Fatalf("first request on newly selected websocket auth must be canonical: %s", payloads[1])
}
if got := gjson.GetBytes(payloads[2], "previous_response_id").String(); got != "resp-2" {
t.Fatalf("stable pinned websocket previous_response_id = %q, want resp-2: %s", got, payloads[2])
}
input := gjson.GetBytes(payloads[2], "input").Array()
if len(input) != 1 || input[0].Get("id").String() != "msg-3" {
t.Fatalf("stable pinned websocket request is not incremental: %s", payloads[2])
}
}
func TestResponsesWebsocketReplaysImmediatelyAfterPinnedAuthFailure(t *testing.T) {
gin.SetMode(gin.TestMode)
tests := []struct {
name string
status int
backupWebsocket bool
}{
{name: "unauthorized to websocket", status: http.StatusUnauthorized, backupWebsocket: true},
{name: "unauthorized to http", status: http.StatusUnauthorized, backupWebsocket: false},
{name: "rate limit to websocket", status: http.StatusTooManyRequests, backupWebsocket: true},
{name: "rate limit to http", status: http.StatusTooManyRequests, backupWebsocket: false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
modelName := fmt.Sprintf("credential-failure-%d-%t-model", tc.status, tc.backupWebsocket)
selector := &orderedWebsocketSelector{order: []string{"auth-a", "auth-b"}}
executor := &websocketPinnedFailoverExecutor{failStatus: tc.status}
manager := coreauth.NewManager(nil, selector, nil)
manager.RegisterExecutor(executor)
authA := &coreauth.Auth{
ID: "auth-a",
Provider: executor.Identifier(),
Status: coreauth.StatusActive,
Attributes: map[string]string{"websockets": "true"},
}
if _, err := manager.Register(context.Background(), authA); err != nil {
t.Fatalf("Register auth A: %v", err)
}
authB := &coreauth.Auth{
ID: "auth-b",
Provider: executor.Identifier(),
Status: coreauth.StatusActive,
Attributes: map[string]string{"websockets": strconv.FormatBool(tc.backupWebsocket)},
}
if _, err := manager.Register(context.Background(), authB); err != nil {
t.Fatalf("Register auth B: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(authA.ID, authA.Provider, []*registry.ModelInfo{{ID: modelName}})
registry.GetGlobalRegistry().RegisterClient(authB.ID, authB.Provider, []*registry.ModelInfo{{ID: modelName}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(authA.ID)
registry.GetGlobalRegistry().UnregisterClient(authB.ID)
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() { _ = conn.Close() }()
firstRequest := fmt.Sprintf(`{"type":"response.create","model":%q,"input":[{"type":"message","id":"msg-1"}]}`, modelName)
if errWrite := conn.WriteMessage(websocket.TextMessage, []byte(firstRequest)); errWrite != nil {
t.Fatalf("write first websocket message: %v", errWrite)
}
if _, payload, errRead := conn.ReadMessage(); errRead != nil || gjson.GetBytes(payload, "type").String() != wsEventTypeCompleted {
t.Fatalf("first websocket response = %s, err=%v", payload, errRead)
}
secondRequest := `{"type":"response.create","previous_response_id":"resp-auth-a-1","input":[{"type":"message","id":"msg-2"}]}`
if errWrite := conn.WriteMessage(websocket.TextMessage, []byte(secondRequest)); errWrite != nil {
t.Fatalf("write second websocket message: %v", errWrite)
}
_, _, errReadClose := conn.ReadMessage()
var replayClose *websocket.CloseError
if !errors.As(errReadClose, &replayClose) || replayClose.Code != websocket.CloseServiceRestart || replayClose.Text != wsHTTPReplayRequiredCloseReason {
t.Fatalf("credential failure response = %v, want replay close %d %q", errReadClose, websocket.CloseServiceRestart, wsHTTPReplayRequiredCloseReason)
}
if got := executor.AuthIDs(); len(got) != 2 || got[0] != "auth-a" || got[1] != "auth-a" {
t.Fatalf("selected auth IDs before replay = %v, want [auth-a auth-a]", got)
}
replayConn, _, errDialReplay := websocket.DefaultDialer.Dial(wsURL, nil)
if errDialReplay != nil {
t.Fatalf("dial replay websocket: %v", errDialReplay)
}
defer func() { _ = replayConn.Close() }()
fullReplay := fmt.Sprintf(`{"type":"response.create","model":%q,"input":[{"type":"message","id":"msg-1"},{"type":"message","id":"out-auth-a-1"},{"type":"message","id":"msg-2"}]}`, modelName)
if errWrite := replayConn.WriteMessage(websocket.TextMessage, []byte(fullReplay)); errWrite != nil {
t.Fatalf("write full replay: %v", errWrite)
}
if _, replayPayload, errReadReplay := replayConn.ReadMessage(); errReadReplay != nil || gjson.GetBytes(replayPayload, "type").String() != wsEventTypeCompleted {
t.Fatalf("full replay response = %s, err=%v", replayPayload, errReadReplay)
}
if got := executor.AuthIDs(); len(got) != 3 || got[2] != "auth-b" {
t.Fatalf("selected auth IDs after replay = %v, want [auth-a auth-a auth-b]", got)
}
authBPayloads := executor.Payloads("auth-b")
if len(authBPayloads) != 1 {
t.Fatalf("auth-b payloads = %d, want 1", len(authBPayloads))
}
authBPayload := authBPayloads[0]
if gjson.GetBytes(authBPayload, "previous_response_id").Exists() || len(gjson.GetBytes(authBPayload, "input").Array()) != 3 {
t.Fatalf("auth-b did not receive full replay: %s", authBPayload)
}
})
}
}
func TestShouldReplayResponsesWebsocketPinnedAuthFailure(t *testing.T) {
cases := []struct {
name string
err *interfaces.ErrorMessage
want bool
}{
{name: "nil", err: nil, want: false},
{name: "unauthorized", err: &interfaces.ErrorMessage{StatusCode: http.StatusUnauthorized}, want: true},
{name: "rate limit", err: &interfaces.ErrorMessage{StatusCode: http.StatusTooManyRequests}, want: true},
{name: "forbidden", err: &interfaces.ErrorMessage{StatusCode: http.StatusForbidden}, want: false},
{name: "service unavailable", err: &interfaces.ErrorMessage{StatusCode: http.StatusServiceUnavailable}, want: false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := shouldReplayResponsesWebsocketPinnedAuthFailure(tc.err); got != tc.want {
t.Fatalf("shouldReplayResponsesWebsocketPinnedAuthFailure() = %v, want %v", got, tc.want)
}
})
}
}
func TestShouldReleaseResponsesWebsocketPinnedAuth(t *testing.T) {
cases := []struct {
name string
err *interfaces.ErrorMessage
want bool
}{
{name: "nil", err: nil, want: false},
{name: "request timeout", err: &interfaces.ErrorMessage{StatusCode: http.StatusRequestTimeout, Error: fmt.Errorf("stream closed before response.completed")}, want: true},
{name: "service unavailable", err: &interfaces.ErrorMessage{StatusCode: http.StatusServiceUnavailable, Error: fmt.Errorf("websocket bootstrap failed")}, want: true},
{name: "bad request", err: &interfaces.ErrorMessage{StatusCode: http.StatusBadRequest, Error: fmt.Errorf("invalid request")}, want: false},
{name: "previous response missing", err: &interfaces.ErrorMessage{StatusCode: http.StatusBadRequest, Error: fmt.Errorf("previous_response_not_found")}, want: true},
{name: "empty stream", err: &interfaces.ErrorMessage{StatusCode: http.StatusInternalServerError, Error: fmt.Errorf("empty_stream: upstream stream closed before first payload")}, want: true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := shouldReleaseResponsesWebsocketPinnedAuth(tc.err); got != tc.want {
t.Fatalf("shouldReleaseResponsesWebsocketPinnedAuth() = %v, want %v", got, tc.want)
}
})
}
}
type websocketPinnedPrematureCloseExecutor struct {
mu sync.Mutex
authIDs []string
calls map[string]int
payloads map[string][][]byte
}
func (e *websocketPinnedPrematureCloseExecutor) Identifier() string { return "xai" }
func (e *websocketPinnedPrematureCloseExecutor) Execute(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{}, errors.New("not implemented")
}
func (e *websocketPinnedPrematureCloseExecutor) ExecuteStream(_ context.Context, auth *coreauth.Auth, req coreexecutor.Request, _ coreexecutor.Options) (*coreexecutor.StreamResult, error) {
authID := ""
if auth != nil {
authID = auth.ID
}
e.mu.Lock()
if e.calls == nil {
e.calls = make(map[string]int)
}
if e.payloads == nil {
e.payloads = make(map[string][][]byte)
}
e.authIDs = append(e.authIDs, authID)
e.calls[authID]++
call := e.calls[authID]
e.payloads[authID] = append(e.payloads[authID], bytes.Clone(req.Payload))
e.mu.Unlock()
if authID == "auth-a" && call == 2 {
chunks := make(chan coreexecutor.StreamChunk, 1)
chunks <- coreexecutor.StreamChunk{Payload: []byte(`{"type":"response.output_item.added","item":{"id":"partial-1","type":"message"}}`)}
close(chunks)
return &coreexecutor.StreamResult{Chunks: chunks}, nil
}
chunks := make(chan coreexecutor.StreamChunk, 1)
chunks <- coreexecutor.StreamChunk{Payload: []byte(fmt.Sprintf(`{"type":"response.completed","response":{"id":"resp-%s-%d","output":[{"type":"message","id":"out-%s-%d"}]}}`, authID, call, authID, call))}
close(chunks)
return &coreexecutor.StreamResult{Chunks: chunks}, nil
}
func (e *websocketPinnedPrematureCloseExecutor) Refresh(_ context.Context, auth *coreauth.Auth) (*coreauth.Auth, error) {
return auth, nil
}
func (e *websocketPinnedPrematureCloseExecutor) CountTokens(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
return coreexecutor.Response{}, errors.New("not implemented")
}
func (e *websocketPinnedPrematureCloseExecutor) HttpRequest(context.Context, *coreauth.Auth, *http.Request) (*http.Response, error) {
return nil, errors.New("not implemented")
}
func (e *websocketPinnedPrematureCloseExecutor) AuthIDs() []string {
e.mu.Lock()
defer e.mu.Unlock()
return append([]string(nil), e.authIDs...)
}
func (e *websocketPinnedPrematureCloseExecutor) Payloads(authID string) [][]byte {
e.mu.Lock()
defer e.mu.Unlock()
src := e.payloads[authID]
out := make([][]byte, len(src))
for i := range src {
out[i] = bytes.Clone(src[i])
}
return out
}
func TestResponsesWebsocketReleasesPinnedAuthAfterStreamClosed408(t *testing.T) {
gin.SetMode(gin.TestMode)
selector := &orderedWebsocketSelector{order: []string{"auth-a", "auth-b"}}
executor := &websocketPinnedPrematureCloseExecutor{}
manager := coreauth.NewManager(nil, selector, nil)
manager.RegisterExecutor(executor)
authA := &coreauth.Auth{
ID: "auth-a",
Provider: executor.Identifier(),
Status: coreauth.StatusActive,
Attributes: map[string]string{"websockets": "true"},
}
if _, err := manager.Register(context.Background(), authA); err != nil {
t.Fatalf("Register auth A: %v", err)
}
authB := &coreauth.Auth{
ID: "auth-b",
Provider: executor.Identifier(),
Status: coreauth.StatusActive,
Attributes: map[string]string{"websockets": "true"},
}
if _, err := manager.Register(context.Background(), authB); err != nil {
t.Fatalf("Register auth B: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(authA.ID, authA.Provider, []*registry.ModelInfo{{ID: "stream-model"}})
registry.GetGlobalRegistry().RegisterClient(authB.ID, authB.Provider, []*registry.ModelInfo{{ID: "stream-model"}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(authA.ID)
registry.GetGlobalRegistry().UnregisterClient(authB.ID)
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() {
if errClose := conn.Close(); errClose != nil {
t.Fatalf("close websocket: %v", errClose)
}
}()
firstRequest := `{"type":"response.create","model":"stream-model","input":[{"type":"message","id":"msg-1"}]}`
if errWrite := conn.WriteMessage(websocket.TextMessage, []byte(firstRequest)); errWrite != nil {
t.Fatalf("write first websocket message: %v", errWrite)
}
if _, payload, errRead := conn.ReadMessage(); errRead != nil || gjson.GetBytes(payload, "type").String() != wsEventTypeCompleted {
t.Fatalf("first websocket response = %s, err=%v", payload, errRead)
}
secondRequest := `{"type":"response.create","previous_response_id":"resp-auth-a-1","input":[{"type":"message","id":"msg-2"}]}`
if errWrite := conn.WriteMessage(websocket.TextMessage, []byte(secondRequest)); errWrite != nil {
t.Fatalf("write second websocket message: %v", errWrite)
}
for {
_, payload, errRead := conn.ReadMessage()
if errRead != nil {
break
}
if gjson.GetBytes(payload, "type").String() == wsEventTypeError {
t.Fatalf("stream transport failure was exposed to the client: %s", payload)
}
}
if got := executor.AuthIDs(); len(got) != 2 || got[0] != "auth-a" || got[1] != "auth-a" {
t.Fatalf("selected auth IDs before replay = %v, want [auth-a auth-a]", got)
}
replayConn, _, errDialReplay := websocket.DefaultDialer.Dial(wsURL, nil)
if errDialReplay != nil {
t.Fatalf("dial replay websocket: %v", errDialReplay)
}
defer func() { _ = replayConn.Close() }()
fullReplay := `{"type":"response.create","model":"stream-model","input":[{"type":"message","id":"msg-1"},{"type":"message","id":"out-auth-a-1"},{"type":"message","id":"msg-2"}]}`
if errWrite := replayConn.WriteMessage(websocket.TextMessage, []byte(fullReplay)); errWrite != nil {
t.Fatalf("write full replay: %v", errWrite)
}
if _, replayResponse, errReadReplay := replayConn.ReadMessage(); errReadReplay != nil || gjson.GetBytes(replayResponse, "type").String() != wsEventTypeCompleted {
t.Fatalf("full replay response = %s, err=%v", replayResponse, errReadReplay)
}
authIDs := executor.AuthIDs()
if len(authIDs) != 3 || authIDs[0] != "auth-a" || authIDs[1] != "auth-a" {
t.Fatalf("selected auth IDs after replay = %v, want auth-a for the first two turns", authIDs)
}
replayAuthID := authIDs[2]
replayPayloads := executor.Payloads(replayAuthID)
replayPayload := replayPayloads[len(replayPayloads)-1]
if gjson.GetBytes(replayPayload, "previous_response_id").Exists() || len(gjson.GetBytes(replayPayload, "input").Array()) != 3 {
t.Fatalf("replay auth %s did not receive full replay: %s", replayAuthID, replayPayload)
}
}
func TestNormalizeResponsesWebsocketRequestTreatsTranscriptReplacementAsReset(t *testing.T) {
lastRequest := []byte(`{"model":"test-model","stream":true,"input":[{"type":"message","id":"msg-1"},{"type":"function_call","id":"fc-1","call_id":"call-1"},{"type":"function_call_output","id":"tool-out-1","call_id":"call-1"},{"type":"message","id":"assistant-1","role":"assistant"}]}`)
lastResponseOutput := []byte(`[
{"type":"message","id":"assistant-1","role":"assistant"}
]`)
raw := []byte(`{"type":"response.create","input":[{"type":"function_call","id":"fc-compact","call_id":"call-1","name":"tool"},{"type":"message","id":"msg-2"}]}`)
normalized, next, errMsg := normalizeResponsesWebsocketRequest(raw, lastRequest, lastResponseOutput)
if errMsg != nil {
t.Fatalf("unexpected error: %v", errMsg.Error)
}
if gjson.GetBytes(normalized, "previous_response_id").Exists() {
t.Fatalf("previous_response_id must not exist in transcript replacement mode")
}
items := gjson.GetBytes(normalized, "input").Array()
if len(items) != 2 {
t.Fatalf("replacement input len = %d, want 2: %s", len(items), normalized)
}
if items[0].Get("id").String() != "fc-compact" || items[1].Get("id").String() != "msg-2" {
t.Fatalf("replacement transcript was not preserved as-is: %s", normalized)
}
if !bytes.Equal(next, normalized) {
t.Fatalf("next request snapshot should match replacement request")
}
}
func TestNormalizeResponsesWebsocketRequestDoesNotTreatDeveloperMessageAsReplacement(t *testing.T) {
lastRequest := []byte(`{"model":"test-model","stream":true,"input":[{"type":"message","id":"msg-1"}]}`)
lastResponseOutput := []byte(`[
{"type":"message","id":"assistant-1","role":"assistant"}
]`)
raw := []byte(`{"type":"response.create","input":[{"type":"message","id":"dev-1","role":"developer"},{"type":"message","id":"msg-2"}]}`)
normalized, next, errMsg := normalizeResponsesWebsocketRequest(raw, lastRequest, lastResponseOutput)
if errMsg != nil {
t.Fatalf("unexpected error: %v", errMsg.Error)
}
items := gjson.GetBytes(normalized, "input").Array()
if len(items) != 4 {
t.Fatalf("merged input len = %d, want 4: %s", len(items), normalized)
}
if items[0].Get("id").String() != "msg-1" ||
items[1].Get("id").String() != "assistant-1" ||
items[2].Get("id").String() != "dev-1" ||
items[3].Get("id").String() != "msg-2" {
t.Fatalf("developer follow-up should preserve merge behavior: %s", normalized)
}
if !bytes.Equal(next, normalized) {
t.Fatalf("next request snapshot should match merged request")
}
}
func TestNormalizeResponsesWebsocketRequestDropsDuplicateFunctionCallsByCallID(t *testing.T) {
lastRequest := []byte(`{"model":"test-model","stream":true,"input":[{"type":"function_call","id":"fc-1","call_id":"call-1"},{"type":"function_call_output","id":"tool-out-1","call_id":"call-1"}]}`)
lastResponseOutput := []byte(`[
{"type":"function_call","id":"fc-1","call_id":"call-1","name":"tool"}
]`)
raw := []byte(`{"type":"response.create","input":[{"type":"message","id":"msg-2"}]}`)
normalized, _, errMsg := normalizeResponsesWebsocketRequest(raw, lastRequest, lastResponseOutput)
if errMsg != nil {
t.Fatalf("unexpected error: %v", errMsg.Error)
}
items := gjson.GetBytes(normalized, "input").Array()
if len(items) != 3 {
t.Fatalf("merged input len = %d, want 3: %s", len(items), normalized)
}
if items[0].Get("id").String() != "fc-1" ||
items[1].Get("id").String() != "tool-out-1" ||
items[2].Get("id").String() != "msg-2" {
t.Fatalf("unexpected merged input order: %s", normalized)
}
}
func TestNormalizeResponsesWebsocketRequestDropsDuplicateInputItemsByID(t *testing.T) {
lastRequest := []byte(`{"model":"test-model","stream":true,"input":[{"type":"message","id":"msg-1","role":"user"}]}`)
lastResponseOutput := []byte(`[
{"type":"function_call","id":"fc-1","call_id":"call-1","name":"tool"}
]`)
raw := []byte(`{"type":"response.create","previous_response_id":"resp-1","input":[{"type":"function_call","id":"fc-1","call_id":"call-2","name":"tool"},{"type":"function_call_output","id":"tool-out-1","call_id":"call-2"}]}`)
normalized, _, errMsg := normalizeResponsesWebsocketRequestWithMode(raw, lastRequest, lastResponseOutput, false, true)
if errMsg != nil {
t.Fatalf("unexpected error: %v", errMsg.Error)
}
items := gjson.GetBytes(normalized, "input").Array()
if len(items) != 3 {
t.Fatalf("merged input len = %d, want 3: %s", len(items), normalized)
}
if items[0].Get("id").String() != "msg-1" ||
items[1].Get("id").String() != "fc-1" ||
items[1].Get("call_id").String() != "call-2" ||
items[2].Get("id").String() != "tool-out-1" {
t.Fatalf("unexpected merged input order: %s", normalized)
}
}
func TestNormalizeResponsesWebsocketRequestTreatsCustomToolTranscriptReplacementAsReset(t *testing.T) {
lastRequest := []byte(`{"model":"test-model","stream":true,"input":[{"type":"message","id":"msg-1"},{"type":"custom_tool_call","id":"ctc-1","call_id":"call-1","name":"apply_patch"},{"type":"custom_tool_call_output","id":"tool-out-1","call_id":"call-1"},{"type":"message","id":"assistant-1","role":"assistant"}]}`)
lastResponseOutput := []byte(`[
{"type":"message","id":"assistant-1","role":"assistant"}
]`)
raw := []byte(`{"type":"response.create","input":[{"type":"custom_tool_call","id":"ctc-compact","call_id":"call-1","name":"apply_patch"},{"type":"custom_tool_call_output","id":"tool-out-compact","call_id":"call-1"},{"type":"message","id":"msg-2"}]}`)
normalized, next, errMsg := normalizeResponsesWebsocketRequest(raw, lastRequest, lastResponseOutput)
if errMsg != nil {
t.Fatalf("unexpected error: %v", errMsg.Error)
}
if gjson.GetBytes(normalized, "previous_response_id").Exists() {
t.Fatalf("previous_response_id must not exist in transcript replacement mode")
}
items := gjson.GetBytes(normalized, "input").Array()
if len(items) != 3 {
t.Fatalf("replacement input len = %d, want 3: %s", len(items), normalized)
}
if items[0].Get("id").String() != "ctc-compact" ||
items[1].Get("id").String() != "tool-out-compact" ||
items[2].Get("id").String() != "msg-2" {
t.Fatalf("replacement transcript was not preserved as-is: %s", normalized)
}
if !bytes.Equal(next, normalized) {
t.Fatalf("next request snapshot should match replacement request")
}
}
func TestNormalizeResponsesWebsocketRequestDropsDuplicateCustomToolCallsByCallID(t *testing.T) {
lastRequest := []byte(`{"model":"test-model","stream":true,"input":[{"type":"custom_tool_call","id":"ctc-1","call_id":"call-1","name":"apply_patch"},{"type":"custom_tool_call_output","id":"tool-out-1","call_id":"call-1"}]}`)
lastResponseOutput := []byte(`[
{"type":"custom_tool_call","id":"ctc-1","call_id":"call-1","name":"apply_patch"}
]`)
raw := []byte(`{"type":"response.create","input":[{"type":"message","id":"msg-2"}]}`)
normalized, _, errMsg := normalizeResponsesWebsocketRequest(raw, lastRequest, lastResponseOutput)
if errMsg != nil {
t.Fatalf("unexpected error: %v", errMsg.Error)
}
items := gjson.GetBytes(normalized, "input").Array()
if len(items) != 3 {
t.Fatalf("merged input len = %d, want 3: %s", len(items), normalized)
}
if items[0].Get("id").String() != "ctc-1" ||
items[1].Get("id").String() != "tool-out-1" ||
items[2].Get("id").String() != "msg-2" {
t.Fatalf("unexpected merged input order: %s", normalized)
}
}
func TestDedupeResponsesWebsocketInputItemsByIDAfterRepair(t *testing.T) {
payload := []byte(`{"input":[{"type":"custom_tool_call","id":"ctc-1","call_id":"call-1","name":"tool"},{"type":"custom_tool_call","id":"ctc-1","call_id":"call-2","name":"tool"},{"type":"custom_tool_call_output","id":"tool-out-1","call_id":"call-2"}]}`)
deduped := dedupeResponsesWebsocketInputItemsByID(payload)
items := gjson.GetBytes(deduped, "input").Array()
if len(items) != 2 {
t.Fatalf("deduped input len = %d, want 2: %s", len(items), deduped)
}
if items[0].Get("id").String() != "ctc-1" ||
items[0].Get("call_id").String() != "call-2" ||
items[1].Get("id").String() != "tool-out-1" {
t.Fatalf("unexpected deduped input: %s", deduped)
}
}
func TestDedupeResponsesWebsocketInputItemsByIDKeepsReferencedToolCall(t *testing.T) {
// Two function_call items share the same id but carry different call_ids
// (e.g. the upstream reused the item id across a re-sent/repaired call).
// Only the first call_id has a matching function_call_output. Deduping by
// id must keep the referenced call so the output is not orphaned, which
// previously triggered an upstream 400 "No tool call found for function
// call output with call_id ...".
payload := []byte(`{"input":[{"type":"function_call","id":"fc-1","call_id":"call-1","name":"exec_command"},{"type":"function_call","id":"fc-1","call_id":"call-2","name":"exec_command"},{"type":"function_call_output","id":"fco-1","call_id":"call-1"}]}`)
deduped := dedupeResponsesWebsocketInputItemsByID(payload)
items := gjson.GetBytes(deduped, "input").Array()
if len(items) != 2 {
t.Fatalf("deduped input len = %d, want 2: %s", len(items), deduped)
}
if items[0].Get("id").String() != "fc-1" ||
items[0].Get("call_id").String() != "call-1" ||
items[1].Get("id").String() != "fco-1" ||
items[1].Get("call_id").String() != "call-1" {
t.Fatalf("unexpected deduped input: %s", deduped)
}
}
func TestResponsesWebsocketCompactionResetsTurnStateOnCustomToolTranscriptReplacement(t *testing.T) {
gin.SetMode(gin.TestMode)
executor := &websocketCompactionCaptureExecutor{}
manager := coreauth.NewManager(nil, nil, nil)
manager.RegisterExecutor(executor)
auth := &coreauth.Auth{ID: "auth-sse", Provider: executor.Identifier(), Status: coreauth.StatusActive}
if _, err := manager.Register(context.Background(), auth); err != nil {
t.Fatalf("Register auth: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: "test-model"}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth.ID)
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
router.POST("/v1/responses/compact", h.Compact)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() {
if errClose := conn.Close(); errClose != nil {
t.Fatalf("close websocket: %v", errClose)
}
}()
requests := []string{
`{"type":"response.create","model":"test-model","input":[{"type":"message","id":"msg-1"}]}`,
`{"type":"response.create","input":[{"type":"custom_tool_call_output","call_id":"call-1","id":"tool-out-1"}]}`,
}
for i := range requests {
if errWrite := conn.WriteMessage(websocket.TextMessage, []byte(requests[i])); errWrite != nil {
t.Fatalf("write websocket message %d: %v", i+1, errWrite)
}
_, payload, errReadMessage := conn.ReadMessage()
if errReadMessage != nil {
t.Fatalf("read websocket message %d: %v", i+1, errReadMessage)
}
if got := gjson.GetBytes(payload, "type").String(); got != wsEventTypeCompleted {
t.Fatalf("message %d payload type = %s, want %s", i+1, got, wsEventTypeCompleted)
}
}
compactResp, errPost := server.Client().Post(
server.URL+"/v1/responses/compact",
"application/json",
strings.NewReader(`{"model":"test-model","input":[{"type":"message","id":"summary-1"}]}`),
)
if errPost != nil {
t.Fatalf("compact request failed: %v", errPost)
}
if errClose := compactResp.Body.Close(); errClose != nil {
t.Fatalf("close compact response body: %v", errClose)
}
if compactResp.StatusCode != http.StatusOK {
t.Fatalf("compact status = %d, want %d", compactResp.StatusCode, http.StatusOK)
}
postCompact := `{"type":"response.create","input":[{"type":"custom_tool_call","id":"ctc-compact","call_id":"call-1","name":"apply_patch"},{"type":"custom_tool_call_output","id":"tool-out-compact","call_id":"call-1"},{"type":"message","id":"msg-2"}]}`
if errWrite := conn.WriteMessage(websocket.TextMessage, []byte(postCompact)); errWrite != nil {
t.Fatalf("write post-compact websocket message: %v", errWrite)
}
_, payload, errReadMessage := conn.ReadMessage()
if errReadMessage != nil {
t.Fatalf("read post-compact websocket message: %v", errReadMessage)
}
if got := gjson.GetBytes(payload, "type").String(); got != wsEventTypeCompleted {
t.Fatalf("post-compact payload type = %s, want %s", got, wsEventTypeCompleted)
}
executor.mu.Lock()
defer executor.mu.Unlock()
if executor.compactPayload == nil {
t.Fatalf("compact payload was not captured")
}
if len(executor.streamPayloads) != 3 {
t.Fatalf("stream payload count = %d, want 3", len(executor.streamPayloads))
}
merged := executor.streamPayloads[2]
items := gjson.GetBytes(merged, "input").Array()
if len(items) != 3 {
t.Fatalf("merged input len = %d, want 3: %s", len(items), merged)
}
if items[0].Get("id").String() != "ctc-compact" ||
items[1].Get("id").String() != "tool-out-compact" ||
items[2].Get("id").String() != "msg-2" {
t.Fatalf("unexpected post-compact input order: %s", merged)
}
if items[0].Get("call_id").String() != "call-1" {
t.Fatalf("post-compact custom tool call id = %s, want call-1", items[0].Get("call_id").String())
}
}
func TestResponsesWebsocketCompactionResetsTurnStateOnTranscriptReplacement(t *testing.T) {
gin.SetMode(gin.TestMode)
executor := &websocketCompactionCaptureExecutor{}
manager := coreauth.NewManager(nil, nil, nil)
manager.RegisterExecutor(executor)
auth := &coreauth.Auth{ID: "auth-sse", Provider: executor.Identifier(), Status: coreauth.StatusActive}
if _, err := manager.Register(context.Background(), auth); err != nil {
t.Fatalf("Register auth: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: "test-model"}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth.ID)
})
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
h := NewOpenAIResponsesAPIHandler(base)
router := gin.New()
router.GET("/v1/responses/ws", h.ResponsesWebsocket)
router.POST("/v1/responses/compact", h.Compact)
server := httptest.NewServer(router)
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/v1/responses/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial websocket: %v", err)
}
defer func() {
if errClose := conn.Close(); errClose != nil {
t.Fatalf("close websocket: %v", errClose)
}
}()
requests := []string{
`{"type":"response.create","model":"test-model","input":[{"type":"message","id":"msg-1"}]}`,
`{"type":"response.create","input":[{"type":"function_call_output","call_id":"call-1","id":"tool-out-1"}]}`,
}
for i := range requests {
if errWrite := conn.WriteMessage(websocket.TextMessage, []byte(requests[i])); errWrite != nil {
t.Fatalf("write websocket message %d: %v", i+1, errWrite)
}
_, payload, errReadMessage := conn.ReadMessage()
if errReadMessage != nil {
t.Fatalf("read websocket message %d: %v", i+1, errReadMessage)
}
if got := gjson.GetBytes(payload, "type").String(); got != wsEventTypeCompleted {
t.Fatalf("message %d payload type = %s, want %s", i+1, got, wsEventTypeCompleted)
}
}
compactResp, errPost := server.Client().Post(
server.URL+"/v1/responses/compact",
"application/json",
strings.NewReader(`{"model":"test-model","input":[{"type":"message","id":"summary-1"}]}`),
)
if errPost != nil {
t.Fatalf("compact request failed: %v", errPost)
}
if errClose := compactResp.Body.Close(); errClose != nil {
t.Fatalf("close compact response body: %v", errClose)
}
if compactResp.StatusCode != http.StatusOK {
t.Fatalf("compact status = %d, want %d", compactResp.StatusCode, http.StatusOK)
}
// Simulate a post-compaction client turn that replaces local history with a compacted transcript.
// The websocket handler must treat this as a state reset, not append it to stale pre-compaction state.
postCompact := `{"type":"response.create","input":[{"type":"function_call","id":"fc-compact","call_id":"call-1","name":"tool"},{"type":"message","id":"msg-2"}]}`
if errWrite := conn.WriteMessage(websocket.TextMessage, []byte(postCompact)); errWrite != nil {
t.Fatalf("write post-compact websocket message: %v", errWrite)
}
_, payload, errReadMessage := conn.ReadMessage()
if errReadMessage != nil {
t.Fatalf("read post-compact websocket message: %v", errReadMessage)
}
if got := gjson.GetBytes(payload, "type").String(); got != wsEventTypeCompleted {
t.Fatalf("post-compact payload type = %s, want %s", got, wsEventTypeCompleted)
}
executor.mu.Lock()
defer executor.mu.Unlock()
if executor.compactPayload == nil {
t.Fatalf("compact payload was not captured")
}
if len(executor.streamPayloads) != 3 {
t.Fatalf("stream payload count = %d, want 3", len(executor.streamPayloads))
}
merged := executor.streamPayloads[2]
items := gjson.GetBytes(merged, "input").Array()
if len(items) != 2 {
t.Fatalf("merged input len = %d, want 2: %s", len(items), merged)
}
if items[0].Get("id").String() != "fc-compact" ||
items[1].Get("id").String() != "msg-2" {
t.Fatalf("unexpected post-compact input order: %s", merged)
}
if items[0].Get("call_id").String() != "call-1" {
t.Fatalf("post-compact function call id = %s, want call-1", items[0].Get("call_id").String())
}
}
func TestInputContainsFullTranscriptFalseForAssistantMessageOnly(t *testing.T) {
input := gjson.Parse(`[
{"type":"message","role":"user","content":"hello"},
{"type":"message","role":"assistant","content":"hi there"}
]`)
if inputContainsFullTranscript(input) {
t.Fatal("assistant message alone must not be treated as full transcript")
}
}
func TestInputContainsFullTranscriptDetectsCompactionItem(t *testing.T) {
for _, typ := range []string{"compaction", "compaction_summary"} {
input := gjson.Parse(`[{"type":"message","role":"user","content":"hello"},{"type":"` + typ + `","encrypted_content":"summary"}]`)
if !inputContainsFullTranscript(input) {
t.Fatalf("expected full transcript for type=%s", typ)
}
}
}
func TestInputContainsFullTranscriptFalseForIncremental(t *testing.T) {
// Normal incremental turns: user messages or function_call_output only.
for _, raw := range []string{
`[{"type":"function_call_output","call_id":"call-1","output":"result"}]`,
`[{"type":"message","role":"user","content":"next question"}]`,
`[]`,
} {
if inputContainsFullTranscript(gjson.Parse(raw)) {
t.Fatalf("incremental input must not be detected as full transcript: %s", raw)
}
}
}
func TestNormalizeSubsequentRequestCompactSkipsMerge(t *testing.T) {
lastRequest := []byte(`{"model":"gpt-5.4","stream":true,"input":[
{"type":"message","role":"user","id":"msg-1","content":"original long prompt"},
{"type":"message","role":"assistant","id":"msg-2","content":"original long response"},
{"type":"function_call","id":"fc-1","call_id":"call-old","name":"bash","arguments":"{}"},
{"type":"function_call_output","id":"fco-1","call_id":"call-old","output":"old result"}
]}`)
lastResponseOutput := []byte(`[
{"type":"message","role":"assistant","id":"msg-3","content":"another assistant reply"},
{"type":"function_call","id":"fc-2","call_id":"call-stale","name":"read","arguments":"{}"}
]`)
// Remote compact response: user messages + compaction item, NO assistant message.
// This is the primary compact scenario from Codex CLI.
raw := []byte(`{"type":"response.create","input":[
{"type":"message","role":"user","id":"msg-1c","content":"compacted user msg"},
{"type":"compaction","encrypted_content":"conversation summary"}
]}`)
normalized, _, errMsg := normalizeResponsesWebsocketRequest(raw, lastRequest, lastResponseOutput)
if errMsg != nil {
t.Fatalf("unexpected error: %v", errMsg.Error)
}
input := gjson.GetBytes(normalized, "input").Array()
if len(input) != 2 {
t.Fatalf("input len = %d, want 2 (compacted only); stale state was not skipped", len(input))
}
if input[0].Get("id").String() != "msg-1c" {
t.Fatalf("input[0].id = %q, want %q", input[0].Get("id").String(), "msg-1c")
}
if input[1].Get("type").String() != "compaction" {
t.Fatalf("input[1].type = %q, want %q", input[1].Get("type").String(), "compaction")
}
}
func TestNormalizeSubsequentRequestReasoningContinuationWithPreviousResponseID(t *testing.T) {
lastRequest := []byte(`{"model":"gpt-5.6-terra","stream":true,"input":[{"type":"message","role":"user","id":"old-user","content":"long history"}]}`)
lastResponseOutput := []byte(`[{"type":"function_call","id":"old-call","call_id":"old-call","name":"lookup","arguments":"{}"}]`)
for _, requestType := range []string{"response.create", "response.append"} {
t.Run(requestType, func(t *testing.T) {
raw := []byte(`{"type":"` + requestType + `","previous_response_id":"resp-1","input":[
{"type":"reasoning","id":"reasoning-1","summary":[]},
{"type":"function_call_output","id":"output-1","call_id":"old-call","output":"result"}
]}`)
normalized, _, errMsg := normalizeResponsesWebsocketRequest(raw, lastRequest, lastResponseOutput)
if errMsg != nil {
t.Fatalf("unexpected error: %v", errMsg.Error)
}
if got := gjson.GetBytes(normalized, "previous_response_id").String(); got != "resp-1" {
t.Fatalf("previous_response_id = %q, want resp-1; payload=%s", got, normalized)
}
input := gjson.GetBytes(normalized, "input").Array()
if len(input) != 2 || input[0].Get("id").String() != "reasoning-1" || input[1].Get("id").String() != "output-1" {
t.Fatalf("incremental continuation was replaced or merged: %s", normalized)
}
})
}
}
func TestResponsesWebsocketOutputCollectorRestoresCompletedOutput(t *testing.T) {
outputItemsByIndex := make(map[int64][]byte)
var outputItemsFallback [][]byte
for _, payload := range [][]byte{
[]byte(`{"type":"response.output_item.done","output_index":1,"item":{"type":"message","id":"reply-1","role":"assistant"}}`),
[]byte(`{"type":"response.output_item.done","output_index":0,"item":{"type":"reasoning","id":"summary-1","summary":[]}}`),
[]byte(`{"type":"response.output_item.done","item":{"type":"function_call","id":"call-1","call_id":"call-1","name":"exec","arguments":"{}"}}`),
} {
collectResponsesWebsocketOutputItem(payload, outputItemsByIndex, &outputItemsFallback)
}
output := responseCompletedOutputFromPayload(
[]byte(`{"type":"response.completed","response":{"id":"resp-1","output":[]}}`),
outputItemsByIndex,
outputItemsFallback,
)
items := gjson.ParseBytes(output).Array()
if len(items) != 3 {
t.Fatalf("collected output len = %d, want 3: %s", len(items), output)
}
wantIDs := []string{"summary-1", "reply-1", "call-1"}
for i, wantID := range wantIDs {
if got := items[i].Get("id").String(); got != wantID {
t.Fatalf("output[%d].id = %q, want %q: %s", i, got, wantID, output)
}
}
}
func TestNormalizeSubsequentRequestCompactMergesWhenCompactionReplayUnsupported(t *testing.T) {
lastRequest := []byte(`{"model":"gpt-5.4","stream":true,"input":[
{"type":"message","role":"user","id":"msg-1","content":"original long prompt"},
{"type":"message","role":"assistant","id":"msg-2","content":"original long response"},
{"type":"function_call","id":"fc-1","call_id":"call-old","name":"bash","arguments":"{}"},
{"type":"function_call_output","id":"fco-1","call_id":"call-old","output":"old result"}
]}`)
lastResponseOutput := []byte(`[
{"type":"message","role":"assistant","id":"msg-3","content":"another assistant reply"},
{"type":"function_call","id":"fc-2","call_id":"call-stale","name":"read","arguments":"{}"}
]`)
raw := []byte(`{"type":"response.create","input":[
{"type":"message","role":"user","id":"msg-1c","content":"compacted user msg"},
{"type":"compaction","encrypted_content":"conversation summary"}
]}`)
normalized, _, errMsg := normalizeResponsesWebsocketRequestWithMode(raw, lastRequest, lastResponseOutput, false, false)
if errMsg != nil {
t.Fatalf("unexpected error: %v", errMsg.Error)
}
input := gjson.GetBytes(normalized, "input").Array()
if len(input) != 7 {
t.Fatalf("input len = %d, want 7 (merged fallback without compaction items)", len(input))
}
wantIDs := []string{"msg-1", "msg-2", "fc-1", "fco-1", "msg-3", "fc-2", "msg-1c"}
for i, want := range wantIDs {
got := input[i].Get("id").String()
if got != want {
t.Fatalf("input[%d].id = %q, want %q", i, got, want)
}
}
for _, item := range input {
if item.Get("type").String() == "compaction" || item.Get("type").String() == "compaction_summary" {
t.Fatalf("compaction items must be stripped for unsupported downstream fallback: %s", item.Raw)
}
}
}
func TestNormalizeSubsequentRequestDropsConsumedCompactionTrigger(t *testing.T) {
lastRequest := []byte(`{"model":"gpt-5.6-sol","stream":true,"input":[
{"type":"message","role":"user","id":"msg-old","content":"old prompt"}
]}`)
triggerRequest := []byte(`{"type":"response.create","previous_response_id":"resp-before-compact","input":[
{"type":"message","role":"user","id":"msg-tool-output","content":"done"},
{"type":"compaction_trigger"}
]}`)
_, stateAfterTrigger, errMsg := normalizeResponsesWebsocketRequestWithMode(triggerRequest, lastRequest, nil, false, false)
if errMsg != nil {
t.Fatalf("normalize trigger request: %v", errMsg.Error)
}
compactionOutput := []byte(`[
{"type":"compaction","id":"cmp-1","encrypted_content":"opaque"}
]`)
replayRequest := []byte(`{"type":"response.create","input":[
{"type":"message","role":"developer","id":"msg-new-context","content":"new context"},
{"type":"compaction","id":"cmp-1","encrypted_content":"opaque"},
{"type":"message","role":"user","id":"msg-next","content":"continue"}
]}`)
normalized, _, errMsg := normalizeResponsesWebsocketRequestWithMode(replayRequest, stateAfterTrigger, compactionOutput, false, false)
if errMsg != nil {
t.Fatalf("normalize compact replay: %v", errMsg.Error)
}
for _, item := range gjson.GetBytes(normalized, "input").Array() {
if item.Get("type").String() == "compaction_trigger" {
t.Fatalf("consumed compaction_trigger was replayed: %s", normalized)
}
}
}
func TestNormalizeSubsequentRequestIncrementalInputStillMerges(t *testing.T) {
// Normal incremental flow: user sends function_call_output (no assistant message).
lastRequest := []byte(`{"model":"gpt-5.4","stream":true,"input":[
{"type":"message","role":"user","id":"msg-1","content":"hello"}
]}`)
lastResponseOutput := []byte(`[
{"type":"message","role":"assistant","id":"msg-2","content":"let me check"},
{"type":"function_call","id":"fc-1","call_id":"call-1","name":"bash","arguments":"{}"}
]`)
raw := []byte(`{"type":"response.create","input":[
{"type":"function_call_output","call_id":"call-1","id":"fco-1","output":"done"}
]}`)
normalized, _, errMsg := normalizeResponsesWebsocketRequest(raw, lastRequest, lastResponseOutput)
if errMsg != nil {
t.Fatalf("unexpected error: %v", errMsg.Error)
}
input := gjson.GetBytes(normalized, "input").Array()
// Should be merged: msg-1 + msg-2 + fc-1 + fco-1 = 4 items
if len(input) != 4 {
t.Fatalf("input len = %d, want 4 (merged)", len(input))
}
wantIDs := []string{"msg-1", "msg-2", "fc-1", "fco-1"}
for i, want := range wantIDs {
got := input[i].Get("id").String()
if got != want {
t.Fatalf("input[%d].id = %q, want %q", i, got, want)
}
}
}
func TestNormalizeSubsequentRequestAssistantInputTriggersTranscriptReplacement(t *testing.T) {
// After dev's shouldReplaceWebsocketTranscript, assistant messages in input
// trigger transcript replacement (no merge with prior state).
lastRequest := []byte(`{"model":"gpt-5.4","stream":true,"input":[
{"type":"message","role":"user","id":"msg-1","content":"hello"}
]}`)
lastResponseOutput := []byte(`[
{"type":"message","role":"assistant","id":"msg-2","content":"prior assistant"},
{"type":"function_call","id":"fc-1","call_id":"call-1","name":"bash","arguments":"{}"}
]`)
raw := []byte(`{"type":"response.append","input":[
{"type":"message","role":"assistant","id":"msg-3","content":"patched assistant turn"}
]}`)
normalized, _, errMsg := normalizeResponsesWebsocketRequest(raw, lastRequest, lastResponseOutput)
if errMsg != nil {
t.Fatalf("unexpected error: %v", errMsg.Error)
}
input := gjson.GetBytes(normalized, "input").Array()
if len(input) != 1 {
t.Fatalf("input len = %d, want 1 (transcript replacement, not merge)", len(input))
}
if input[0].Get("id").String() != "msg-3" {
t.Fatalf("input[0].id = %q, want %q", input[0].Get("id").String(), "msg-3")
}
}