package executor import ( "bytes" "context" "errors" "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/gorilla/websocket" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" "github.com/tidwall/gjson") func TestXAIWebsocketsEnabledForConfigAPIKey(t *testing.T) { auth := &cliproxyauth.Auth{ Provider: "xai", Attributes: map[string]string{ "api_key": "xai-key", "websockets": "true", }, } if !xaiWebsocketsEnabled(auth) { t.Fatal("xaiWebsocketsEnabled() = false, want true") }} func TestXAIAutoExecutorRequiredUpstreamWebsocketRejectsHTTPFallback(t *testing.T) { exec := NewXAIAutoExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ ID: "xai-http-only", Provider: "xai", Attributes: map[string]string{ "api_key": "xai-key", }, } ctx := cliproxyexecutor.WithRequiredUpstreamWebsocket( cliproxyexecutor.WithDownstreamWebsocket(context.Background()), ) _, errExecute := exec.ExecuteStream(ctx, auth, cliproxyexecutor.Request{ Model: "grok-4", Payload: []byte(`{"model":"grok-4","previous_response_id":"resp-1","input":[{"type":"message","id":"msg-2"}]}`), }, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("openai-response")}) if errExecute == nil { t.Fatal("ExecuteStream() error = nil, want replay-required error") } statusErr, ok := errExecute.(interface{ StatusCode() int }) if !ok || statusErr.StatusCode() != http.StatusUpgradeRequired { t.Fatalf("ExecuteStream() error = %T %v, want status 426", errExecute, errExecute) } if got := gjson.Get(errExecute.Error(), "error.code").String(); got != "upstream_http_replay_required" { t.Fatalf("ExecuteStream() error code = %q, want upstream_http_replay_required", got) } requestScoped, ok := errExecute.(cliproxyexecutor.RequestScopedError) if !ok || !requestScoped.IsRequestScoped() { t.Fatalf("ExecuteStream() error = %T, want request-scoped replay signal", errExecute) }} func TestXAIWebsocketsRequiredUpstreamRejectsCompactionHTTPFallback(t *testing.T) { exec := NewXAIWebsocketsExecutor(&config.Config{}) ctx := cliproxyexecutor.WithRequiredUpstreamWebsocket(context.Background()) _, errExecute := exec.ExecuteStream(ctx, &cliproxyauth.Auth{}, cliproxyexecutor.Request{ Model: "grok-4", Payload: []byte(`{"model":"grok-4","input":[{"type":"compaction_trigger"}]}`), }, cliproxyexecutor.Options{}) if !cliproxyexecutor.IsUpstreamWebsocketReplayRequired(errExecute) { t.Fatalf("ExecuteStream() error = %T %v, want replay-required", errExecute, errExecute) }} func TestXAIWebsocketMissingRequiredSessionDoesNotMarkUpstreamAttempt(t *testing.T) { exec := NewXAIWebsocketsExecutor(&config.Config{}) exec.store = &codexWebsocketSessionStore{sessions: make(map[string]*codexWebsocketSession)} auth := &cliproxyauth.Auth{ ID: "xai-required-session", Provider: "xai", Attributes: map[string]string{ "api_key": "xai-key", }, } ctx := cliproxyexecutor.WithUpstreamAttemptTracker( cliproxyexecutor.WithRequiredUpstreamWebsocket(context.Background()), ) _, errExecute := exec.ExecuteStream(ctx, auth, cliproxyexecutor.Request{ Model: "grok-4", Payload: []byte(`{"model":"grok-4","previous_response_id":"resp-1","input":[{"type":"message","role":"user","content":"hello"}]}`), }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, Metadata: map[string]any{ cliproxyexecutor.ExecutionSessionMetadataKey: "missing-xai-session", }, }) if !cliproxyexecutor.IsUpstreamWebsocketReplayRequired(errExecute) { t.Fatalf("ExecuteStream() error = %T %v, want replay-required", errExecute, errExecute) } if cliproxyexecutor.UpstreamAttempted(ctx) { t.Fatal("missing retained websocket connection was marked as an upstream attempt") }} func TestXAIWebsocketSuccessfulHandshakeDoesNotMarkRequestAttempt(t *testing.T) { upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { conn, errUpgrade := upgrader.Upgrade(w, r, nil) if errUpgrade != nil { t.Errorf("upgrade websocket: %v", errUpgrade) return } defer func() { _ = conn.Close() }() _, _, _ = conn.ReadMessage() })) defer server.Close() exec := NewXAIWebsocketsExecutor(&config.Config{}) ctx := cliproxyexecutor.WithUpstreamAttemptTracker(context.Background()) conn, closer, resp, errDial := exec.dialXAIWebsocket(ctx, &cliproxyauth.Auth{}, strings.Replace(server.URL, "http", "ws", 1), http.Header{}) if errDial != nil || conn == nil { t.Fatalf("dialXAIWebsocket() = (%p, %v), want successful connection", conn, errDial) } if resp != nil && resp.Body != nil { defer func() { _ = resp.Body.Close() }() } defer func() { _ = closer.Close() }() if cliproxyexecutor.UpstreamAttempted(ctx) { t.Fatal("successful handshake was marked before an upstream request was sent") }} func TestMapXAIWebsocketWriteErrorStopsRetryForMessageTooBig(t *testing.T) { networkWriteErr := errors.New("write: broken pipe") tests := []struct { name string closeCode int writeErr error wantStatus int wantRetry bool }{ { name: "close sent after message too big is request scoped", closeCode: websocket.CloseMessageTooBig, writeErr: websocket.ErrCloseSent, wantStatus: http.StatusRequestEntityTooLarge, wantRetry: false, }, { name: "network write error after message too big is request scoped", closeCode: websocket.CloseMessageTooBig, writeErr: networkWriteErr, wantStatus: http.StatusRequestEntityTooLarge, wantRetry: false, }, { name: "other close keeps stale connection retry", closeCode: websocket.CloseNormalClosure, writeErr: websocket.ErrCloseSent, wantRetry: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { sess := &codexWebsocketSession{} conn := &websocket.Conn{} sess.resetUpstreamDisconnectError(conn) sess.setUpstreamDisconnectError(conn, &websocket.CloseError{Code: tt.closeCode}) mappedErr := mapXAIWebsocketWriteError(sess, conn, tt.writeErr) if got := shouldRetryXAIWebsocketSend(mappedErr); got != tt.wantRetry { t.Fatalf("shouldRetryXAIWebsocketSend() = %v, want %v; err=%v", got, tt.wantRetry, mappedErr) } if tt.wantStatus == 0 { if !errors.Is(mappedErr, tt.writeErr) { t.Fatalf("mapped error = %v, want %v", mappedErr, tt.writeErr) } return } statusErr, ok := mappedErr.(interface{ StatusCode() int }) if !ok || statusErr.StatusCode() != tt.wantStatus { t.Fatalf("mapped status = %v, want %d; err=%v", statusErr, tt.wantStatus, mappedErr) } requestErr, ok := mappedErr.(interface{ IsRequestScoped() bool }) if !ok || !requestErr.IsRequestScoped() { t.Fatalf("mapped error should be request scoped, got %T", mappedErr) } }) }} func TestXAIWebsocketsExecuteStreamMapsMessageTooBigClose(t *testing.T) { upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { t.Errorf("upgrade websocket: %v", err) return } defer func() { _ = conn.Close() }() if _, _, errRead := conn.ReadMessage(); errRead != nil { t.Errorf("read upstream websocket message: %v", errRead) return } deadline := time.Now().Add(time.Second) closeMessage := websocket.FormatCloseMessage(websocket.CloseMessageTooBig, "message too big") if errWrite := conn.WriteControl(websocket.CloseMessage, closeMessage, deadline); errWrite != nil { t.Errorf("write close websocket message: %v", errWrite) } })) defer server.Close() exec := NewXAIWebsocketsExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Provider: "xai", Attributes: map[string]string{ "base_url": server.URL, "websockets": "true", }, Metadata: map[string]any{"access_token": "xai-token"}, } req := cliproxyexecutor.Request{ Model: "grok-4.5", Payload: []byte(`{"model":"grok-4.5","input":[{"type":"message","role":"user","content":"hello"}]}`), } opts := cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, ResponseFormat: sdktranslator.FormatOpenAIResponse, } result, err := exec.ExecuteStream(context.Background(), auth, req, opts) if err != nil { t.Fatalf("ExecuteStream() error = %v", err) } select { case chunk, ok := <-result.Chunks: if !ok { t.Fatal("stream closed before error chunk") } if chunk.Err == nil { t.Fatal("error chunk Err = nil, want message-too-big error") } statusErr, ok := chunk.Err.(interface{ StatusCode() int }) if !ok || statusErr.StatusCode() != http.StatusRequestEntityTooLarge { t.Fatalf("status error = %v, want %d; err=%v", statusErr, http.StatusRequestEntityTooLarge, chunk.Err) } if got := gjson.Get(chunk.Err.Error(), "error.code").String(); got != "message_too_big" { t.Fatalf("error code = %q, want message_too_big; err=%v", got, chunk.Err) } requestErr, ok := chunk.Err.(interface{ IsRequestScoped() bool }) if !ok || !requestErr.IsRequestScoped() { t.Fatalf("message-too-big error should be request scoped, got %T", chunk.Err) } case <-time.After(5 * time.Second): t.Fatal("timed out waiting for error stream chunk") }} func TestXAIWebsocketsExecuteStreamSendsResponseCreateWithPreviousResponseID(t *testing.T) { upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }} capturedPayload := make(chan []byte, 1) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/responses" { t.Errorf("path = %q, want /responses", r.URL.Path) } if got := r.Header.Get("Authorization"); got != "Bearer xai-token" { t.Errorf("Authorization = %q, want Bearer xai-token", got) } if got := r.Header.Get("x-grok-conv-id"); got != "execution-session-1" { t.Errorf("x-grok-conv-id = %q, want execution-session-1", got) } conn, err := upgrader.Upgrade(w, r, nil) if err != nil { t.Errorf("upgrade websocket: %v", err) return } defer func() { _ = conn.Close() }() _, payload, errRead := conn.ReadMessage() if errRead != nil { t.Errorf("read upstream websocket message: %v", errRead) return } capturedPayload <- bytes.Clone(payload) completed := []byte(`{"type":"response.completed","response":{"id":"resp-xai-1","output":[],"usage":{"input_tokens":0,"output_tokens":0,"total_tokens":0}}}`) if errWrite := conn.WriteMessage(websocket.TextMessage, completed); errWrite != nil { t.Errorf("write completed websocket message: %v", errWrite) } })) defer server.Close() exec := NewXAIWebsocketsExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ ID: "xai-auth", Provider: "xai", Attributes: map[string]string{ "base_url": server.URL, "websockets": "true", }, Metadata: map[string]any{"access_token": "xai-token"}, } req := cliproxyexecutor.Request{ Model: "grok-4.3", Payload: []byte(`{"model":"grok-4.3","stream":true,"previous_response_id":"resp-prev","instructions":"system prompt","input":[{"type":"message","role":"user","content":"hello"}]}`), } opts := cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, ResponseFormat: sdktranslator.FormatOpenAIResponse, Metadata: map[string]any{ cliproxyexecutor.ExecutionSessionMetadataKey: "execution-session-1", }, } ctx := cliproxyexecutor.WithUpstreamAttemptTracker( cliproxyexecutor.WithDownstreamWebsocket(context.Background()), ) result, err := exec.ExecuteStream(ctx, auth, req, opts) if err != nil { t.Fatalf("ExecuteStream() error = %v", err) } if !cliproxyexecutor.UpstreamAttempted(ctx) { t.Fatal("websocket write did not mark an upstream attempt") } select { case payload := <-capturedPayload: if got := gjson.GetBytes(payload, "type").String(); got != "response.create" { t.Fatalf("type = %q, want response.create; payload=%s", got, payload) } if got := gjson.GetBytes(payload, "previous_response_id").String(); got != "resp-prev" { t.Fatalf("previous_response_id = %q, want resp-prev; payload=%s", got, payload) } if gjson.GetBytes(payload, "stream").Exists() { t.Fatalf("stream must be omitted for xAI websocket payload: %s", payload) } if gjson.GetBytes(payload, "instructions").Exists() { t.Fatalf("instructions must be omitted when previous_response_id is set: %s", payload) } if got := gjson.GetBytes(payload, "prompt_cache_key").String(); got != "execution-session-1" { t.Fatalf("prompt_cache_key = %q, want execution-session-1; payload=%s", got, payload) } if got := gjson.GetBytes(payload, "store").Bool(); !got { t.Fatalf("store = false, want true; payload=%s", payload) } case <-time.After(5 * time.Second): t.Fatal("timed out waiting for upstream websocket payload") } select { case chunk, ok := <-result.Chunks: if !ok { t.Fatal("stream closed before completed chunk") } if chunk.Err != nil { t.Fatalf("chunk error = %v", chunk.Err) } if got := gjson.GetBytes(bytes.TrimSpace(chunk.Payload), "type").String(); got != "response.completed" { t.Fatalf("chunk type = %q, want response.completed; payload=%s", got, chunk.Payload) } case <-time.After(5 * time.Second): t.Fatal("timed out waiting for completed chunk") }} func TestXAIWebsocketsExecuteStreamRestoresNamespaceToolCalls(t *testing.T) { upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }} capturedPayload := make(chan []byte, 1) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { t.Errorf("upgrade websocket: %v", err) return } defer func() { _ = conn.Close() }() _, payload, errRead := conn.ReadMessage() if errRead != nil { t.Errorf("read upstream websocket message: %v", errRead) return } capturedPayload <- bytes.Clone(payload) events := [][]byte{ []byte(`{"type":"response.output_item.done","output_index":0,"item":{"type":"function_call","name":"mcp__exa__web_search_exa","call_id":"call_1","arguments":"{}"}}`), []byte(`{"type":"response.completed","response":{"id":"resp_1","output":[],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}`), } for _, event := range events { if errWrite := conn.WriteMessage(websocket.TextMessage, event); errWrite != nil { t.Errorf("write websocket event: %v", errWrite) return } } })) defer server.Close() exec := NewXAIWebsocketsExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Provider: "xai", Attributes: map[string]string{ "base_url": server.URL, "websockets": "true", }, Metadata: map[string]any{"access_token": "xai-token"}, } req := cliproxyexecutor.Request{ Model: "grok-4.3", Payload: []byte(`{ "model":"grok-4.3", "input":[ {"type":"additional_tools","role":"developer","tools":[{ "type":"namespace", "name":"mcp__exa", "tools":[{"type":"function","name":"web_search_exa","parameters":{"type":"object"}}] }]}, {"role":"user","content":"use Exa"} ] }`), } opts := cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, ResponseFormat: sdktranslator.FormatOpenAIResponse, Stream: true, } ctx := cliproxyexecutor.WithDownstreamWebsocket(context.Background()) result, err := exec.ExecuteStream(ctx, auth, req, opts) if err != nil { t.Fatalf("ExecuteStream() error = %v", err) } select { case payload := <-capturedPayload: for _, item := range gjson.GetBytes(payload, "input").Array() { if got := item.Get("type").String(); got == "additional_tools" { t.Fatalf("upstream input contains unsupported additional_tools item: %s", payload) } } if got := gjson.GetBytes(payload, "input.0.role").String(); got != "user" { t.Fatalf("input.0.role = %q, want user; payload=%s", got, payload) } tool := gjson.GetBytes(payload, "tools.0") if got := tool.Get("name").String(); got != "mcp__exa__web_search_exa" { t.Fatalf("upstream tool name = %q, want qualified name; payload=%s", got, payload) } if tool.Get("tools").Exists() { t.Fatalf("upstream tool should not contain namespace children: %s", payload) } case <-time.After(5 * time.Second): t.Fatal("timed out waiting for upstream websocket payload") } var outputItemDone, completed gjson.Result for chunk := range result.Chunks { if chunk.Err != nil { t.Fatalf("stream chunk error = %v", chunk.Err) } payload := gjson.ParseBytes(bytes.TrimSpace(chunk.Payload)) switch payload.Get("type").String() { case "response.output_item.done": outputItemDone = payload case "response.completed": completed = payload } } for label, item := range map[string]gjson.Result{ "output_item.done": outputItemDone.Get("item"), "completed": completed.Get("response.output.0"), } { if got := item.Get("name").String(); got != "web_search_exa" { t.Fatalf("%s name = %q, want child name; item=%s", label, got, item.Raw) } if got := item.Get("namespace").String(); got != "mcp__exa" { t.Fatalf("%s namespace = %q, want mcp__exa; item=%s", label, got, item.Raw) } }} func TestXAIWebsocketsExecuteStreamPreservesClientSameNameToolsWithXSearch(t *testing.T) { upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { t.Errorf("upgrade websocket: %v", err) return } defer func() { _ = conn.Close() }() if _, _, errRead := conn.ReadMessage(); errRead != nil { t.Errorf("read upstream websocket message: %v", errRead) returnLines 1–500 of 2177. Load more
package executor import ( "bytes" "context" "errors" "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/gorilla/websocket" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" "github.com/tidwall/gjson" ) func TestXAIWebsocketsEnabledForConfigAPIKey(t *testing.T) { auth := &cliproxyauth.Auth{ Provider: "xai", Attributes: map[string]string{ "api_key": "xai-key", "websockets": "true", }, } if !xaiWebsocketsEnabled(auth) { t.Fatal("xaiWebsocketsEnabled() = false, want true") } } func TestXAIAutoExecutorRequiredUpstreamWebsocketRejectsHTTPFallback(t *testing.T) { exec := NewXAIAutoExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ ID: "xai-http-only", Provider: "xai", Attributes: map[string]string{ "api_key": "xai-key", }, } ctx := cliproxyexecutor.WithRequiredUpstreamWebsocket( cliproxyexecutor.WithDownstreamWebsocket(context.Background()), ) _, errExecute := exec.ExecuteStream(ctx, auth, cliproxyexecutor.Request{ Model: "grok-4", Payload: []byte(`{"model":"grok-4","previous_response_id":"resp-1","input":[{"type":"message","id":"msg-2"}]}`), }, cliproxyexecutor.Options{SourceFormat: sdktranslator.FromString("openai-response")}) if errExecute == nil { t.Fatal("ExecuteStream() error = nil, want replay-required error") } statusErr, ok := errExecute.(interface{ StatusCode() int }) if !ok || statusErr.StatusCode() != http.StatusUpgradeRequired { t.Fatalf("ExecuteStream() error = %T %v, want status 426", errExecute, errExecute) } if got := gjson.Get(errExecute.Error(), "error.code").String(); got != "upstream_http_replay_required" { t.Fatalf("ExecuteStream() error code = %q, want upstream_http_replay_required", got) } requestScoped, ok := errExecute.(cliproxyexecutor.RequestScopedError) if !ok || !requestScoped.IsRequestScoped() { t.Fatalf("ExecuteStream() error = %T, want request-scoped replay signal", errExecute) } } func TestXAIWebsocketsRequiredUpstreamRejectsCompactionHTTPFallback(t *testing.T) { exec := NewXAIWebsocketsExecutor(&config.Config{}) ctx := cliproxyexecutor.WithRequiredUpstreamWebsocket(context.Background()) _, errExecute := exec.ExecuteStream(ctx, &cliproxyauth.Auth{}, cliproxyexecutor.Request{ Model: "grok-4", Payload: []byte(`{"model":"grok-4","input":[{"type":"compaction_trigger"}]}`), }, cliproxyexecutor.Options{}) if !cliproxyexecutor.IsUpstreamWebsocketReplayRequired(errExecute) { t.Fatalf("ExecuteStream() error = %T %v, want replay-required", errExecute, errExecute) } } func TestXAIWebsocketMissingRequiredSessionDoesNotMarkUpstreamAttempt(t *testing.T) { exec := NewXAIWebsocketsExecutor(&config.Config{}) exec.store = &codexWebsocketSessionStore{sessions: make(map[string]*codexWebsocketSession)} auth := &cliproxyauth.Auth{ ID: "xai-required-session", Provider: "xai", Attributes: map[string]string{ "api_key": "xai-key", }, } ctx := cliproxyexecutor.WithUpstreamAttemptTracker( cliproxyexecutor.WithRequiredUpstreamWebsocket(context.Background()), ) _, errExecute := exec.ExecuteStream(ctx, auth, cliproxyexecutor.Request{ Model: "grok-4", Payload: []byte(`{"model":"grok-4","previous_response_id":"resp-1","input":[{"type":"message","role":"user","content":"hello"}]}`), }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, Metadata: map[string]any{ cliproxyexecutor.ExecutionSessionMetadataKey: "missing-xai-session", }, }) if !cliproxyexecutor.IsUpstreamWebsocketReplayRequired(errExecute) { t.Fatalf("ExecuteStream() error = %T %v, want replay-required", errExecute, errExecute) } if cliproxyexecutor.UpstreamAttempted(ctx) { t.Fatal("missing retained websocket connection was marked as an upstream attempt") } } func TestXAIWebsocketSuccessfulHandshakeDoesNotMarkRequestAttempt(t *testing.T) { upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { conn, errUpgrade := upgrader.Upgrade(w, r, nil) if errUpgrade != nil { t.Errorf("upgrade websocket: %v", errUpgrade) return } defer func() { _ = conn.Close() }() _, _, _ = conn.ReadMessage() })) defer server.Close() exec := NewXAIWebsocketsExecutor(&config.Config{}) ctx := cliproxyexecutor.WithUpstreamAttemptTracker(context.Background()) conn, closer, resp, errDial := exec.dialXAIWebsocket(ctx, &cliproxyauth.Auth{}, strings.Replace(server.URL, "http", "ws", 1), http.Header{}) if errDial != nil || conn == nil { t.Fatalf("dialXAIWebsocket() = (%p, %v), want successful connection", conn, errDial) } if resp != nil && resp.Body != nil { defer func() { _ = resp.Body.Close() }() } defer func() { _ = closer.Close() }() if cliproxyexecutor.UpstreamAttempted(ctx) { t.Fatal("successful handshake was marked before an upstream request was sent") } } func TestMapXAIWebsocketWriteErrorStopsRetryForMessageTooBig(t *testing.T) { networkWriteErr := errors.New("write: broken pipe") tests := []struct { name string closeCode int writeErr error wantStatus int wantRetry bool }{ { name: "close sent after message too big is request scoped", closeCode: websocket.CloseMessageTooBig, writeErr: websocket.ErrCloseSent, wantStatus: http.StatusRequestEntityTooLarge, wantRetry: false, }, { name: "network write error after message too big is request scoped", closeCode: websocket.CloseMessageTooBig, writeErr: networkWriteErr, wantStatus: http.StatusRequestEntityTooLarge, wantRetry: false, }, { name: "other close keeps stale connection retry", closeCode: websocket.CloseNormalClosure, writeErr: websocket.ErrCloseSent, wantRetry: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { sess := &codexWebsocketSession{} conn := &websocket.Conn{} sess.resetUpstreamDisconnectError(conn) sess.setUpstreamDisconnectError(conn, &websocket.CloseError{Code: tt.closeCode}) mappedErr := mapXAIWebsocketWriteError(sess, conn, tt.writeErr) if got := shouldRetryXAIWebsocketSend(mappedErr); got != tt.wantRetry { t.Fatalf("shouldRetryXAIWebsocketSend() = %v, want %v; err=%v", got, tt.wantRetry, mappedErr) } if tt.wantStatus == 0 { if !errors.Is(mappedErr, tt.writeErr) { t.Fatalf("mapped error = %v, want %v", mappedErr, tt.writeErr) } return } statusErr, ok := mappedErr.(interface{ StatusCode() int }) if !ok || statusErr.StatusCode() != tt.wantStatus { t.Fatalf("mapped status = %v, want %d; err=%v", statusErr, tt.wantStatus, mappedErr) } requestErr, ok := mappedErr.(interface{ IsRequestScoped() bool }) if !ok || !requestErr.IsRequestScoped() { t.Fatalf("mapped error should be request scoped, got %T", mappedErr) } }) } } func TestXAIWebsocketsExecuteStreamMapsMessageTooBigClose(t *testing.T) { upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { t.Errorf("upgrade websocket: %v", err) return } defer func() { _ = conn.Close() }() if _, _, errRead := conn.ReadMessage(); errRead != nil { t.Errorf("read upstream websocket message: %v", errRead) return } deadline := time.Now().Add(time.Second) closeMessage := websocket.FormatCloseMessage(websocket.CloseMessageTooBig, "message too big") if errWrite := conn.WriteControl(websocket.CloseMessage, closeMessage, deadline); errWrite != nil { t.Errorf("write close websocket message: %v", errWrite) } })) defer server.Close() exec := NewXAIWebsocketsExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Provider: "xai", Attributes: map[string]string{ "base_url": server.URL, "websockets": "true", }, Metadata: map[string]any{"access_token": "xai-token"}, } req := cliproxyexecutor.Request{ Model: "grok-4.5", Payload: []byte(`{"model":"grok-4.5","input":[{"type":"message","role":"user","content":"hello"}]}`), } opts := cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, ResponseFormat: sdktranslator.FormatOpenAIResponse, } result, err := exec.ExecuteStream(context.Background(), auth, req, opts) if err != nil { t.Fatalf("ExecuteStream() error = %v", err) } select { case chunk, ok := <-result.Chunks: if !ok { t.Fatal("stream closed before error chunk") } if chunk.Err == nil { t.Fatal("error chunk Err = nil, want message-too-big error") } statusErr, ok := chunk.Err.(interface{ StatusCode() int }) if !ok || statusErr.StatusCode() != http.StatusRequestEntityTooLarge { t.Fatalf("status error = %v, want %d; err=%v", statusErr, http.StatusRequestEntityTooLarge, chunk.Err) } if got := gjson.Get(chunk.Err.Error(), "error.code").String(); got != "message_too_big" { t.Fatalf("error code = %q, want message_too_big; err=%v", got, chunk.Err) } requestErr, ok := chunk.Err.(interface{ IsRequestScoped() bool }) if !ok || !requestErr.IsRequestScoped() { t.Fatalf("message-too-big error should be request scoped, got %T", chunk.Err) } case <-time.After(5 * time.Second): t.Fatal("timed out waiting for error stream chunk") } } func TestXAIWebsocketsExecuteStreamSendsResponseCreateWithPreviousResponseID(t *testing.T) { upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }} capturedPayload := make(chan []byte, 1) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/responses" { t.Errorf("path = %q, want /responses", r.URL.Path) } if got := r.Header.Get("Authorization"); got != "Bearer xai-token" { t.Errorf("Authorization = %q, want Bearer xai-token", got) } if got := r.Header.Get("x-grok-conv-id"); got != "execution-session-1" { t.Errorf("x-grok-conv-id = %q, want execution-session-1", got) } conn, err := upgrader.Upgrade(w, r, nil) if err != nil { t.Errorf("upgrade websocket: %v", err) return } defer func() { _ = conn.Close() }() _, payload, errRead := conn.ReadMessage() if errRead != nil { t.Errorf("read upstream websocket message: %v", errRead) return } capturedPayload <- bytes.Clone(payload) completed := []byte(`{"type":"response.completed","response":{"id":"resp-xai-1","output":[],"usage":{"input_tokens":0,"output_tokens":0,"total_tokens":0}}}`) if errWrite := conn.WriteMessage(websocket.TextMessage, completed); errWrite != nil { t.Errorf("write completed websocket message: %v", errWrite) } })) defer server.Close() exec := NewXAIWebsocketsExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ ID: "xai-auth", Provider: "xai", Attributes: map[string]string{ "base_url": server.URL, "websockets": "true", }, Metadata: map[string]any{"access_token": "xai-token"}, } req := cliproxyexecutor.Request{ Model: "grok-4.3", Payload: []byte(`{"model":"grok-4.3","stream":true,"previous_response_id":"resp-prev","instructions":"system prompt","input":[{"type":"message","role":"user","content":"hello"}]}`), } opts := cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, ResponseFormat: sdktranslator.FormatOpenAIResponse, Metadata: map[string]any{ cliproxyexecutor.ExecutionSessionMetadataKey: "execution-session-1", }, } ctx := cliproxyexecutor.WithUpstreamAttemptTracker( cliproxyexecutor.WithDownstreamWebsocket(context.Background()), ) result, err := exec.ExecuteStream(ctx, auth, req, opts) if err != nil { t.Fatalf("ExecuteStream() error = %v", err) } if !cliproxyexecutor.UpstreamAttempted(ctx) { t.Fatal("websocket write did not mark an upstream attempt") } select { case payload := <-capturedPayload: if got := gjson.GetBytes(payload, "type").String(); got != "response.create" { t.Fatalf("type = %q, want response.create; payload=%s", got, payload) } if got := gjson.GetBytes(payload, "previous_response_id").String(); got != "resp-prev" { t.Fatalf("previous_response_id = %q, want resp-prev; payload=%s", got, payload) } if gjson.GetBytes(payload, "stream").Exists() { t.Fatalf("stream must be omitted for xAI websocket payload: %s", payload) } if gjson.GetBytes(payload, "instructions").Exists() { t.Fatalf("instructions must be omitted when previous_response_id is set: %s", payload) } if got := gjson.GetBytes(payload, "prompt_cache_key").String(); got != "execution-session-1" { t.Fatalf("prompt_cache_key = %q, want execution-session-1; payload=%s", got, payload) } if got := gjson.GetBytes(payload, "store").Bool(); !got { t.Fatalf("store = false, want true; payload=%s", payload) } case <-time.After(5 * time.Second): t.Fatal("timed out waiting for upstream websocket payload") } select { case chunk, ok := <-result.Chunks: if !ok { t.Fatal("stream closed before completed chunk") } if chunk.Err != nil { t.Fatalf("chunk error = %v", chunk.Err) } if got := gjson.GetBytes(bytes.TrimSpace(chunk.Payload), "type").String(); got != "response.completed" { t.Fatalf("chunk type = %q, want response.completed; payload=%s", got, chunk.Payload) } case <-time.After(5 * time.Second): t.Fatal("timed out waiting for completed chunk") } } func TestXAIWebsocketsExecuteStreamRestoresNamespaceToolCalls(t *testing.T) { upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }} capturedPayload := make(chan []byte, 1) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { t.Errorf("upgrade websocket: %v", err) return } defer func() { _ = conn.Close() }() _, payload, errRead := conn.ReadMessage() if errRead != nil { t.Errorf("read upstream websocket message: %v", errRead) return } capturedPayload <- bytes.Clone(payload) events := [][]byte{ []byte(`{"type":"response.output_item.done","output_index":0,"item":{"type":"function_call","name":"mcp__exa__web_search_exa","call_id":"call_1","arguments":"{}"}}`), []byte(`{"type":"response.completed","response":{"id":"resp_1","output":[],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}`), } for _, event := range events { if errWrite := conn.WriteMessage(websocket.TextMessage, event); errWrite != nil { t.Errorf("write websocket event: %v", errWrite) return } } })) defer server.Close() exec := NewXAIWebsocketsExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Provider: "xai", Attributes: map[string]string{ "base_url": server.URL, "websockets": "true", }, Metadata: map[string]any{"access_token": "xai-token"}, } req := cliproxyexecutor.Request{ Model: "grok-4.3", Payload: []byte(`{ "model":"grok-4.3", "input":[ {"type":"additional_tools","role":"developer","tools":[{ "type":"namespace", "name":"mcp__exa", "tools":[{"type":"function","name":"web_search_exa","parameters":{"type":"object"}}] }]}, {"role":"user","content":"use Exa"} ] }`), } opts := cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, ResponseFormat: sdktranslator.FormatOpenAIResponse, Stream: true, } ctx := cliproxyexecutor.WithDownstreamWebsocket(context.Background()) result, err := exec.ExecuteStream(ctx, auth, req, opts) if err != nil { t.Fatalf("ExecuteStream() error = %v", err) } select { case payload := <-capturedPayload: for _, item := range gjson.GetBytes(payload, "input").Array() { if got := item.Get("type").String(); got == "additional_tools" { t.Fatalf("upstream input contains unsupported additional_tools item: %s", payload) } } if got := gjson.GetBytes(payload, "input.0.role").String(); got != "user" { t.Fatalf("input.0.role = %q, want user; payload=%s", got, payload) } tool := gjson.GetBytes(payload, "tools.0") if got := tool.Get("name").String(); got != "mcp__exa__web_search_exa" { t.Fatalf("upstream tool name = %q, want qualified name; payload=%s", got, payload) } if tool.Get("tools").Exists() { t.Fatalf("upstream tool should not contain namespace children: %s", payload) } case <-time.After(5 * time.Second): t.Fatal("timed out waiting for upstream websocket payload") } var outputItemDone, completed gjson.Result for chunk := range result.Chunks { if chunk.Err != nil { t.Fatalf("stream chunk error = %v", chunk.Err) } payload := gjson.ParseBytes(bytes.TrimSpace(chunk.Payload)) switch payload.Get("type").String() { case "response.output_item.done": outputItemDone = payload case "response.completed": completed = payload } } for label, item := range map[string]gjson.Result{ "output_item.done": outputItemDone.Get("item"), "completed": completed.Get("response.output.0"), } { if got := item.Get("name").String(); got != "web_search_exa" { t.Fatalf("%s name = %q, want child name; item=%s", label, got, item.Raw) } if got := item.Get("namespace").String(); got != "mcp__exa" { t.Fatalf("%s namespace = %q, want mcp__exa; item=%s", label, got, item.Raw) } } } func TestXAIWebsocketsExecuteStreamPreservesClientSameNameToolsWithXSearch(t *testing.T) { upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { t.Errorf("upgrade websocket: %v", err) return } defer func() { _ = conn.Close() }() if _, _, errRead := conn.ReadMessage(); errRead != nil { t.Errorf("read upstream websocket message: %v", errRead) return