From 40c456c168bed4d9c13f8eb3d3b6ca465601a248 Mon Sep 17 00:00:00 2001 From: Derek Legenzoff Date: Wed, 17 Jun 2026 12:25:38 -0700 Subject: [PATCH 1/4] Add capi.disableWebSocketResponses session option Add the capi.disableWebSocketResponses opt-out to session create/resume across all six SDK languages, so consumers in proxy/WebSocket-blocked environments can fall back to the HTTP Responses transport for the CAPI Responses API. SDK-side follow-up to github/copilot-agent-runtime#10551, which makes WebSocket transport the default for CAPI and adds this opt-out. The field is a hand-written pass-through mirroring the existing provider (BYOK) nested option, wired into session.create and session.resume. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Client.cs | 5 + dotnet/src/Types.cs | 27 ++++ dotnet/test/Unit/CloneTests.cs | 28 ++++ dotnet/test/Unit/SerializationTests.cs | 70 +++++++++ go/client.go | 2 + go/client_test.go | 125 +++++++++++++++ go/types.go | 21 +++ .../github/copilot/SessionRequestBuilder.java | 2 + .../copilot/rpc/CapiSessionOptions.java | 63 ++++++++ .../copilot/rpc/CreateSessionRequest.java | 13 ++ .../copilot/rpc/ResumeSessionConfig.java | 28 ++++ .../copilot/rpc/ResumeSessionRequest.java | 13 ++ .../com/github/copilot/rpc/SessionConfig.java | 28 ++++ .../copilot/CapiSessionOptionsTest.java | 131 ++++++++++++++++ .../copilot/JsonIncludeNonNullTest.java | 6 + nodejs/src/client.ts | 2 + nodejs/src/index.ts | 1 + nodejs/src/types.ts | 32 ++++ nodejs/test/client.test.ts | 32 ++++ python/copilot/__init__.py | 2 + python/copilot/client.py | 41 +++++ python/test_client.py | 41 +++++ rust/src/types.rs | 144 +++++++++++++++++- rust/src/wire.rs | 6 +- 24 files changed, 857 insertions(+), 6 deletions(-) create mode 100644 java/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java create mode 100644 java/src/test/java/com/github/copilot/CapiSessionOptionsTest.java diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 85fb8bd34..06a139dd2 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -963,6 +963,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance toolFilter.AvailableTools, toolFilter.ExcludedTools, config.Provider, + config.Capi, config.EnableSessionTelemetry, config.OnPermissionRequest != null ? true : null, config.OnUserInputRequest != null ? true : null, @@ -1159,6 +1160,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes toolFilter.AvailableTools, toolFilter.ExcludedTools, config.Provider, + config.Capi, config.EnableSessionTelemetry, config.OnPermissionRequest != null ? true : null, config.OnUserInputRequest != null ? true : null, @@ -2355,6 +2357,7 @@ internal record CreateSessionRequest( IList? AvailableTools, IList? ExcludedTools, ProviderConfig? Provider, + CapiSessionOptions? Capi, bool? EnableSessionTelemetry, bool? RequestPermission, bool? RequestUserInput, @@ -2445,6 +2448,7 @@ internal record ResumeSessionRequest( IList? AvailableTools, IList? ExcludedTools, ProviderConfig? Provider, + CapiSessionOptions? Capi, bool? EnableSessionTelemetry, bool? RequestPermission, bool? RequestUserInput, @@ -2569,6 +2573,7 @@ internal record HooksInvokeResponse( [JsonSerializable(typeof(EmbeddingCacheStorageMode))] [JsonSerializable(typeof(ModelCapabilitiesOverride))] [JsonSerializable(typeof(ProviderConfig))] + [JsonSerializable(typeof(CapiSessionOptions))] [JsonSerializable(typeof(ResumeSessionRequest))] [JsonSerializable(typeof(ResumeSessionResponse))] [JsonSerializable(typeof(SessionCapabilities))] diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 706a1ec6b..abbdc97b8 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -2058,6 +2058,26 @@ public sealed class ProviderConfig public int? MaxOutputTokens { get; set; } } +/// +/// Provider-scoped options for the CAPI (Copilot API) provider. +/// +public sealed class CapiSessionOptions +{ + /// + /// When , opts out of the WebSocket transport for the CAPI Responses API + /// and uses the HTTP Responses transport instead. + /// + /// + /// WebSocket transport is the default for CAPI Responses API requests when the model advertises + /// the ws:/responses endpoint. Set this option for users behind proxies where WebSockets + /// fail. This is equivalent to setting the COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES + /// environment variable. The option is scoped under the capi namespace because a single + /// session can host multiple providers, such as CAPI and BYOK, so transport choice is provider-level. + /// + [JsonPropertyName("disableWebSocketResponses")] + public bool? DisableWebSocketResponses { get; set; } +} + /// /// Azure OpenAI-specific provider options. /// @@ -2494,6 +2514,7 @@ protected SessionConfigBase(SessionConfigBase? other) OnPermissionRequest = other.OnPermissionRequest; OnUserInputRequest = other.OnUserInputRequest; Provider = other.Provider; + Capi = other.Capi; EnableSessionTelemetry = other.EnableSessionTelemetry; SkipCustomInstructions = other.SkipCustomInstructions; CustomAgentsLocalOnly = other.CustomAgentsLocalOnly; @@ -2649,6 +2670,11 @@ protected SessionConfigBase(SessionConfigBase? other) /// Custom model provider configuration for the session. public ProviderConfig? Provider { get; set; } + /// + /// CAPI (Copilot API) provider-scoped configuration for the session. + /// + public CapiSessionOptions? Capi { get; set; } + /// /// Enables or disables internal session telemetry for this session. /// When false, disables session telemetry. When null (the default) or true, @@ -3554,6 +3580,7 @@ public sealed class SystemMessageTransformRpcResponse [JsonSerializable(typeof(PingRequest))] [JsonSerializable(typeof(PingResponse))] [JsonSerializable(typeof(ProviderConfig))] +[JsonSerializable(typeof(CapiSessionOptions))] [JsonSerializable(typeof(SessionContext))] [JsonSerializable(typeof(SessionLifecycleEvent))] [JsonSerializable(typeof(SessionLifecycleEventMetadata))] diff --git a/dotnet/test/Unit/CloneTests.cs b/dotnet/test/Unit/CloneTests.cs index 2894952cb..1081ea0aa 100644 --- a/dotnet/test/Unit/CloneTests.cs +++ b/dotnet/test/Unit/CloneTests.cs @@ -82,6 +82,7 @@ public void SessionConfig_Clone_CopiesAllProperties() McpOAuthTokenStorage = McpOAuthTokenStorageMode.Persistent, CustomAgents = [new CustomAgentConfig { Name = "agent1", Model = "claude-haiku-4.5" }], Agent = "agent1", + Capi = new CapiSessionOptions { DisableWebSocketResponses = true }, Cloud = new CloudSessionOptions { Repository = new CloudSessionRepository @@ -123,6 +124,7 @@ public void SessionConfig_Clone_CopiesAllProperties() Assert.Equal(original.CustomAgents.Count, clone.CustomAgents!.Count); Assert.Equal(original.CustomAgents[0].Model, clone.CustomAgents[0].Model); Assert.Equal(original.Agent, clone.Agent); + Assert.Same(original.Capi, clone.Capi); Assert.Same(original.Cloud, clone.Cloud); Assert.Equal(original.DefaultAgent!.ExcludedTools, clone.DefaultAgent!.ExcludedTools); Assert.Equal(original.SkillDirectories, clone.SkillDirectories); @@ -515,4 +517,30 @@ public void ResumeSessionConfig_Clone_CopiesMcpOAuthTokenStorage() Assert.Equal(McpOAuthTokenStorageMode.Persistent, clone.McpOAuthTokenStorage); } + + [Fact] + public void SessionConfig_Clone_CopiesCapiOptions() + { + var original = new SessionConfig + { + Capi = new CapiSessionOptions { DisableWebSocketResponses = true }, + }; + + var clone = original.Clone(); + + Assert.Same(original.Capi, clone.Capi); + } + + [Fact] + public void ResumeSessionConfig_Clone_CopiesCapiOptions() + { + var original = new ResumeSessionConfig + { + Capi = new CapiSessionOptions { DisableWebSocketResponses = true }, + }; + + var clone = original.Clone(); + + Assert.Same(original.Capi, clone.Capi); + } } diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index f074a4d2f..4a6dc21e5 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -50,6 +50,25 @@ public void ProviderConfig_CanSerializeHeaders_WithSdkOptions() Assert.Equal(4096, deserialized.MaxOutputTokens); } + [Fact] + public void CapiSessionOptions_CanSerializeDisableWebSocketResponses_WithSdkOptions() + { + var options = GetSerializerOptions(); + var original = new CapiSessionOptions + { + DisableWebSocketResponses = true + }; + + var json = JsonSerializer.Serialize(original, options); + using var document = JsonDocument.Parse(json); + var root = document.RootElement; + Assert.True(root.GetProperty("disableWebSocketResponses").GetBoolean()); + + var deserialized = JsonSerializer.Deserialize(json, options); + Assert.NotNull(deserialized); + Assert.True(deserialized.DisableWebSocketResponses); + } + [Fact] public void ModelBilling_CanSerializeTokenPrices_WithSdkOptions() { @@ -221,6 +240,57 @@ public void ResumeSessionRequest_CanSerializeInstructionDirectories_WithSdkOptio Assert.Equal("C:\\resume-instructions", root.GetProperty("instructionDirectories")[0].GetString()); } + [Fact] + public void SessionRequests_CanSerializeCapiOptions_WithSdkOptions() + { + var options = GetSerializerOptions(); + var capi = new CapiSessionOptions { DisableWebSocketResponses = true }; + + var createRequestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); + var createRequest = CreateInternalRequest( + createRequestType, + ("SessionId", "session-id"), + ("Capi", capi)); + + var createJson = JsonSerializer.Serialize(createRequest, createRequestType, options); + using var createDocument = JsonDocument.Parse(createJson); + Assert.True(createDocument.RootElement.GetProperty("capi").GetProperty("disableWebSocketResponses").GetBoolean()); + + var resumeRequestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); + var resumeRequest = CreateInternalRequest( + resumeRequestType, + ("SessionId", "session-id"), + ("Capi", capi)); + + var resumeJson = JsonSerializer.Serialize(resumeRequest, resumeRequestType, options); + using var resumeDocument = JsonDocument.Parse(resumeJson); + Assert.True(resumeDocument.RootElement.GetProperty("capi").GetProperty("disableWebSocketResponses").GetBoolean()); + } + + [Fact] + public void SessionRequests_OmitCapiOptions_WhenUnset() + { + var options = GetSerializerOptions(); + + var createRequestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); + var createRequest = CreateInternalRequest( + createRequestType, + ("SessionId", "session-id")); + + var createJson = JsonSerializer.Serialize(createRequest, createRequestType, options); + using var createDocument = JsonDocument.Parse(createJson); + Assert.False(createDocument.RootElement.TryGetProperty("capi", out _)); + + var resumeRequestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); + var resumeRequest = CreateInternalRequest( + resumeRequestType, + ("SessionId", "session-id")); + + var resumeJson = JsonSerializer.Serialize(resumeRequest, resumeRequestType, options); + using var resumeDocument = JsonDocument.Parse(resumeJson); + Assert.False(resumeDocument.RootElement.TryGetProperty("capi", out _)); + } + [Fact] public void SessionRequests_CanSerializeReasoningSummary_WithSdkOptions() { diff --git a/go/client.go b/go/client.go index ad330e5a0..32cbc58d0 100644 --- a/go/client.go +++ b/go/client.go @@ -681,6 +681,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.ExcludedTools = excludedTools req.ToolFilterPrecedence = precedence req.Provider = config.Provider + req.Capi = config.Capi req.EnableSessionTelemetry = config.EnableSessionTelemetry req.SkipCustomInstructions = config.SkipCustomInstructions req.CustomAgentsLocalOnly = config.CustomAgentsLocalOnly @@ -976,6 +977,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.SystemMessage = wireSystemMessage req.Tools = config.Tools req.Provider = config.Provider + req.Capi = config.Capi req.EnableSessionTelemetry = config.EnableSessionTelemetry req.SkipCustomInstructions = config.SkipCustomInstructions req.CustomAgentsLocalOnly = config.CustomAgentsLocalOnly diff --git a/go/client_test.go b/go/client_test.go index e54689fa9..a6366c7d1 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -209,6 +209,76 @@ func newRuntimeShutdownRpcPair(t *testing.T) (*jsonrpc2.Client, *jsonrpc2.Client return rpcClient, server, shutdownCalled } +func TestClient_ForwardsCapiOptionsToSessionRequests(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + + createParams := make(chan json.RawMessage, 1) + server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + createParams <- append(json.RawMessage(nil), params...) + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `","workspacePath":"/workspace"}`), nil + }) + + _, err := client.CreateSession(t.Context(), &SessionConfig{ + Capi: &CapiSessionOptions{DisableWebSocketResponses: Bool(true)}, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + assertCapiDisableWebSocketResponses(t, <-createParams) + + resumeParams := make(chan json.RawMessage, 1) + server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + resumeParams <- append(json.RawMessage(nil), params...) + return []byte(`{"sessionId":"resumed-capi","workspacePath":"/workspace"}`), nil + }) + + _, err = client.ResumeSessionWithOptions(t.Context(), "resumed-capi", &ResumeSessionConfig{ + Capi: &CapiSessionOptions{DisableWebSocketResponses: Bool(true)}, + }) + if err != nil { + t.Fatalf("ResumeSessionWithOptions failed: %v", err) + } + assertCapiDisableWebSocketResponses(t, <-resumeParams) +} + +func assertCapiDisableWebSocketResponses(t *testing.T, params json.RawMessage) { + t.Helper() + + var decoded map[string]any + if err := json.Unmarshal(params, &decoded); err != nil { + t.Fatalf("failed to unmarshal request params: %v", err) + } + capi, ok := decoded["capi"].(map[string]any) + if !ok { + t.Fatalf("expected capi object in request params, got %T", decoded["capi"]) + } + if capi["disableWebSocketResponses"] != true { + t.Fatalf("expected capi.disableWebSocketResponses=true, got %v", capi["disableWebSocketResponses"]) + } +} + +func sessionIDFromParams(t *testing.T, params json.RawMessage) string { + t.Helper() + + var decoded struct { + SessionID string `json:"sessionId"` + } + if err := json.Unmarshal(params, &decoded); err != nil { + t.Fatalf("failed to unmarshal request params: %v", err) + } + if decoded.SessionID == "" { + t.Fatal("expected generated sessionId in request params") + } + return decoded.SessionID +} + func assertRuntimeShutdownNotCalled(t *testing.T, shutdownCalled <-chan struct{}) { t.Helper() select { @@ -1339,6 +1409,61 @@ func TestCreateSessionRequest_Cloud(t *testing.T) { }) } +func TestSessionRequests_Capi(t *testing.T) { + t.Run("forwards capi options in session.create RPC", func(t *testing.T) { + req := createSessionRequest{ + Capi: &CapiSessionOptions{DisableWebSocketResponses: Bool(true)}, + } + data, err := json.Marshal(req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + capi, ok := m["capi"].(map[string]any) + if !ok { + t.Fatalf("Expected capi to be an object, got %T", m["capi"]) + } + if capi["disableWebSocketResponses"] != true { + t.Errorf("Expected disableWebSocketResponses=true, got %v", capi["disableWebSocketResponses"]) + } + }) + + t.Run("forwards capi options in session.resume RPC", func(t *testing.T) { + req := resumeSessionRequest{ + SessionID: "s1", + Capi: &CapiSessionOptions{DisableWebSocketResponses: Bool(true)}, + } + data, err := json.Marshal(req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + capi, ok := m["capi"].(map[string]any) + if !ok { + t.Fatalf("Expected capi to be an object, got %T", m["capi"]) + } + if capi["disableWebSocketResponses"] != true { + t.Errorf("Expected disableWebSocketResponses=true, got %v", capi["disableWebSocketResponses"]) + } + }) + + t.Run("omits capi from JSON when unset", func(t *testing.T) { + req := createSessionRequest{} + data, _ := json.Marshal(req) + var m map[string]any + json.Unmarshal(data, &m) + if _, ok := m["capi"]; ok { + t.Error("Expected capi to be omitted when unset") + } + }) +} + func TestResumeSessionRequest_Commands(t *testing.T) { t.Run("forwards commands in session.resume RPC", func(t *testing.T) { req := resumeSessionRequest{ diff --git a/go/types.go b/go/types.go index 5ed0b6931..c3210c447 100644 --- a/go/types.go +++ b/go/types.go @@ -983,6 +983,8 @@ type SessionConfig struct { IncludeSubAgentStreamingEvents *bool // Provider configures a custom model provider (BYOK) Provider *ProviderConfig + // Capi configures provider-scoped CAPI (Copilot API) session options. + Capi *CapiSessionOptions // EnableSessionTelemetry enables or disables internal session telemetry for this session. // When false, disables session telemetry. When nil (the default) or true, // telemetry is enabled for GitHub-authenticated sessions. When a custom @@ -1316,6 +1318,8 @@ type ResumeSessionConfig struct { ExcludedTools []string // Provider configures a custom model provider Provider *ProviderConfig + // Capi configures provider-scoped CAPI (Copilot API) session options. + Capi *CapiSessionOptions // EnableSessionTelemetry enables or disables internal session telemetry for this session. // When false, disables session telemetry. When nil (the default) or true, // telemetry is enabled for GitHub-authenticated sessions. When a custom @@ -1540,6 +1544,21 @@ type ProviderConfig struct { MaxOutputTokens int `json:"maxOutputTokens,omitempty"` } +// CapiSessionOptions configures provider-scoped CAPI (Copilot API) session behavior. +// +// WebSocket transport is the default for the CAPI Responses API whenever the +// model advertises the ws:/responses endpoint. Set DisableWebSocketResponses to +// Bool(true) to opt out to the HTTP Responses transport, which is useful behind +// proxies where WebSockets fail. This is equivalent to setting the +// COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES environment variable. These options +// are provider-scoped under the capi namespace because a single session can host +// multiple providers, such as CAPI and BYOK, so transport choice is provider-level. +type CapiSessionOptions struct { + // DisableWebSocketResponses opts out of the default WebSocket Responses + // transport and uses HTTP Responses transport when set to Bool(true). + DisableWebSocketResponses *bool `json:"disableWebSocketResponses,omitempty"` +} + // AzureProviderOptions contains Azure-specific provider configuration type AzureProviderOptions struct { // APIVersion is the Azure API version. Defaults to "2024-10-21". @@ -1721,6 +1740,7 @@ type createSessionRequest struct { ExcludedTools []string `json:"excludedTools,omitempty"` ToolFilterPrecedence *rpc.OptionsUpdateToolFilterPrecedence `json:"toolFilterPrecedence,omitempty"` Provider *ProviderConfig `json:"provider,omitempty"` + Capi *CapiSessionOptions `json:"capi,omitempty"` EnableSessionTelemetry *bool `json:"enableSessionTelemetry,omitempty"` SkipCustomInstructions *bool `json:"skipCustomInstructions,omitempty"` CustomAgentsLocalOnly *bool `json:"customAgentsLocalOnly,omitempty"` @@ -1800,6 +1820,7 @@ type resumeSessionRequest struct { ExcludedTools []string `json:"excludedTools,omitempty"` ToolFilterPrecedence *rpc.OptionsUpdateToolFilterPrecedence `json:"toolFilterPrecedence,omitempty"` Provider *ProviderConfig `json:"provider,omitempty"` + Capi *CapiSessionOptions `json:"capi,omitempty"` EnableSessionTelemetry *bool `json:"enableSessionTelemetry,omitempty"` SkipCustomInstructions *bool `json:"skipCustomInstructions,omitempty"` CustomAgentsLocalOnly *bool `json:"customAgentsLocalOnly,omitempty"` diff --git a/java/src/main/java/com/github/copilot/SessionRequestBuilder.java b/java/src/main/java/com/github/copilot/SessionRequestBuilder.java index 5697c7060..f4979bba6 100644 --- a/java/src/main/java/com/github/copilot/SessionRequestBuilder.java +++ b/java/src/main/java/com/github/copilot/SessionRequestBuilder.java @@ -113,6 +113,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess request.setAvailableTools(config.getAvailableTools()); request.setExcludedTools(config.getExcludedTools()); request.setProvider(config.getProvider()); + request.setCapi(config.getCapi()); config.getEnableSessionTelemetry().ifPresent(request::setEnableSessionTelemetry); if (config.getOnUserInputRequest() != null) { request.setRequestUserInput(true); @@ -225,6 +226,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo request.setAvailableTools(config.getAvailableTools()); request.setExcludedTools(config.getExcludedTools()); request.setProvider(config.getProvider()); + request.setCapi(config.getCapi()); config.getEnableSessionTelemetry().ifPresent(request::setEnableSessionTelemetry); if (config.getOnUserInputRequest() != null) { request.setRequestUserInput(true); diff --git a/java/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java b/java/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java new file mode 100644 index 000000000..f63e65872 --- /dev/null +++ b/java/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Provider-scoped session options for the CAPI (Copilot API) provider. + *

+ * WebSocket transport is the default for the CAPI Responses API whenever the + * model advertises the {@code ws:/responses} endpoint. Setting + * {@link #setDisableWebSocketResponses(Boolean)} to {@code true} opts out to + * the HTTP Responses transport instead, which is useful for users behind + * proxies where WebSockets fail. This is equivalent to setting the + * {@code COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES} environment variable. + *

+ * These options are scoped under the {@code capi} namespace because a single + * session can host multiple providers (for example, CAPI and BYOK), so + * transport choice is provider-level rather than top-level session state. All + * setter methods return {@code this} for method chaining. + * + * @see SessionConfig#setCapi(CapiSessionOptions) + * @see ResumeSessionConfig#setCapi(CapiSessionOptions) + * @since 1.5.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CapiSessionOptions { + + @JsonProperty("disableWebSocketResponses") + private Boolean disableWebSocketResponses; + + /** + * Gets whether CAPI Responses API WebSocket transport is disabled. + * + * @return {@code true} to opt out of WebSocket Responses transport, + * {@code false} to explicitly allow it, or {@code null} to use the + * default behavior + */ + public Boolean getDisableWebSocketResponses() { + return disableWebSocketResponses; + } + + /** + * Sets whether to disable CAPI Responses API WebSocket transport. + *

+ * WebSocket transport is the default for the CAPI Responses API whenever the + * model advertises the {@code ws:/responses} endpoint. Set this to {@code true} + * to opt out to the HTTP Responses transport instead, which is useful for users + * behind proxies where WebSockets fail. This is equivalent to setting the + * {@code COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES} environment variable. + * + * @param disableWebSocketResponses + * {@code true} to opt out of WebSocket Responses transport + * @return this config for method chaining + */ + public CapiSessionOptions setDisableWebSocketResponses(Boolean disableWebSocketResponses) { + this.disableWebSocketResponses = disableWebSocketResponses; + return this; + } +} diff --git a/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java b/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java index 7211cc36c..a585e36b8 100644 --- a/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java @@ -60,6 +60,9 @@ public final class CreateSessionRequest { @JsonProperty("provider") private ProviderConfig provider; + @JsonProperty("capi") + private CapiSessionOptions capi; + @JsonProperty("enableSessionTelemetry") private Boolean enableSessionTelemetry; @@ -313,6 +316,16 @@ public void setProvider(ProviderConfig provider) { this.provider = provider; } + /** Gets the CAPI session options. @return the CAPI session options */ + public CapiSessionOptions getCapi() { + return capi; + } + + /** Sets the CAPI session options. @param capi the CAPI session options */ + public void setCapi(CapiSessionOptions capi) { + this.capi = capi; + } + /** Gets enable session telemetry flag. @return the flag */ public Boolean getEnableSessionTelemetry() { return enableSessionTelemetry; diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index 680d337ec..5dcfb2249 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -45,6 +45,7 @@ public class ResumeSessionConfig { private List availableTools; private List excludedTools; private ProviderConfig provider; + private CapiSessionOptions capi; private Boolean enableSessionTelemetry; private Boolean skipCustomInstructions; private Boolean customAgentsLocalOnly; @@ -254,6 +255,32 @@ public ResumeSessionConfig setProvider(ProviderConfig provider) { return this; } + /** + * Gets the CAPI provider-scoped session options. + * + * @return the CAPI session options + */ + public CapiSessionOptions getCapi() { + return capi; + } + + /** + * Sets CAPI provider-scoped session options. + *

+ * Use {@link CapiSessionOptions#setDisableWebSocketResponses(Boolean)} to opt + * out of the default CAPI Responses API WebSocket transport and use HTTP + * Responses transport instead. + * + * @param capi + * the CAPI session options + * @return this config for method chaining + * @see CapiSessionOptions + */ + public ResumeSessionConfig setCapi(CapiSessionOptions capi) { + this.capi = capi; + return this; + } + /** * Enables or disables internal session telemetry for this session. When * {@code false}, disables session telemetry. When unset (the default) or @@ -1548,6 +1575,7 @@ public ResumeSessionConfig clone() { copy.availableTools = this.availableTools != null ? new ArrayList<>(this.availableTools) : null; copy.excludedTools = this.excludedTools != null ? new ArrayList<>(this.excludedTools) : null; copy.provider = this.provider; + copy.capi = this.capi; copy.enableSessionTelemetry = this.enableSessionTelemetry; copy.reasoningEffort = this.reasoningEffort; copy.reasoningSummary = this.reasoningSummary; diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java index e88be7a9f..4dc5e775e 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java @@ -62,6 +62,9 @@ public final class ResumeSessionRequest { @JsonProperty("provider") private ProviderConfig provider; + @JsonProperty("capi") + private CapiSessionOptions capi; + @JsonProperty("enableSessionTelemetry") private Boolean enableSessionTelemetry; @@ -318,6 +321,16 @@ public void setProvider(ProviderConfig provider) { this.provider = provider; } + /** Gets the CAPI session options. @return the CAPI session options */ + public CapiSessionOptions getCapi() { + return capi; + } + + /** Sets the CAPI session options. @param capi the CAPI session options */ + public void setCapi(CapiSessionOptions capi) { + this.capi = capi; + } + /** Gets enable session telemetry flag. @return the flag */ public Boolean getEnableSessionTelemetry() { return enableSessionTelemetry; diff --git a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java index ded429867..be766ea7b 100644 --- a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -49,6 +49,7 @@ public class SessionConfig { private List availableTools; private List excludedTools; private ProviderConfig provider; + private CapiSessionOptions capi; private Boolean enableSessionTelemetry; private Boolean skipCustomInstructions; private Boolean customAgentsLocalOnly; @@ -355,6 +356,32 @@ public SessionConfig setProvider(ProviderConfig provider) { return this; } + /** + * Gets the CAPI provider-scoped session options. + * + * @return the CAPI session options + */ + public CapiSessionOptions getCapi() { + return capi; + } + + /** + * Sets CAPI provider-scoped session options. + *

+ * Use {@link CapiSessionOptions#setDisableWebSocketResponses(Boolean)} to opt + * out of the default CAPI Responses API WebSocket transport and use HTTP + * Responses transport instead. + * + * @param capi + * the CAPI session options + * @return this config instance for method chaining + * @see CapiSessionOptions + */ + public SessionConfig setCapi(CapiSessionOptions capi) { + this.capi = capi; + return this; + } + /** * Enables or disables internal session telemetry for this session. When * {@code false}, disables session telemetry. When unset (the default) or @@ -1671,6 +1698,7 @@ public SessionConfig clone() { copy.availableTools = this.availableTools != null ? new ArrayList<>(this.availableTools) : null; copy.excludedTools = this.excludedTools != null ? new ArrayList<>(this.excludedTools) : null; copy.provider = this.provider; + copy.capi = this.capi; copy.enableSessionTelemetry = this.enableSessionTelemetry; copy.skipCustomInstructions = this.skipCustomInstructions; copy.customAgentsLocalOnly = this.customAgentsLocalOnly; diff --git a/java/src/test/java/com/github/copilot/CapiSessionOptionsTest.java b/java/src/test/java/com/github/copilot/CapiSessionOptionsTest.java new file mode 100644 index 000000000..fb27a52dd --- /dev/null +++ b/java/src/test/java/com/github/copilot/CapiSessionOptionsTest.java @@ -0,0 +1,131 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; + +import com.github.copilot.rpc.CapiSessionOptions; +import com.github.copilot.rpc.ResumeSessionConfig; +import com.github.copilot.rpc.SessionConfig; + +/** + * Tests for CAPI provider-scoped session options. + */ +class CapiSessionOptionsTest { + + @Test + void defaultsAreNull() { + var capi = new CapiSessionOptions(); + + assertNull(capi.getDisableWebSocketResponses()); + } + + @Test + void fluentSetterReturnsSameInstance() { + var capi = new CapiSessionOptions(); + + assertSame(capi, capi.setDisableWebSocketResponses(true)); + assertEquals(Boolean.TRUE, capi.getDisableWebSocketResponses()); + } + + @Test + void serializesDisableWebSocketResponses() { + var capi = new CapiSessionOptions().setDisableWebSocketResponses(true); + + JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(capi); + + assertTrue(json.get("disableWebSocketResponses").asBoolean()); + } + + @Test + void omitsUnsetDisableWebSocketResponses() { + var capi = new CapiSessionOptions(); + + JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(capi); + + assertTrue(json.path("disableWebSocketResponses").isMissingNode()); + assertEquals(0, json.size()); + } + + @Test + void createRequestIncludesCapiWhenSet() { + var config = new SessionConfig().setCapi(new CapiSessionOptions().setDisableWebSocketResponses(true)); + + var request = SessionRequestBuilder.buildCreateRequest(config, "session-1"); + JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(request); + + assertNotNull(request.getCapi()); + assertTrue(json.get("capi").get("disableWebSocketResponses").asBoolean()); + } + + @Test + void createRequestOmitsCapiWhenUnset() { + var config = new SessionConfig(); + + var request = SessionRequestBuilder.buildCreateRequest(config, "session-1"); + JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(request); + + assertNull(request.getCapi()); + assertTrue(json.path("capi").isMissingNode()); + } + + @Test + void resumeRequestIncludesCapiWhenSet() { + var config = new ResumeSessionConfig().setCapi(new CapiSessionOptions().setDisableWebSocketResponses(true)); + + var request = SessionRequestBuilder.buildResumeRequest("session-1", config); + JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(request); + + assertNotNull(request.getCapi()); + assertTrue(json.get("capi").get("disableWebSocketResponses").asBoolean()); + } + + @Test + void resumeRequestOmitsCapiWhenUnset() { + var config = new ResumeSessionConfig(); + + var request = SessionRequestBuilder.buildResumeRequest("session-1", config); + JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(request); + + assertNull(request.getCapi()); + assertTrue(json.path("capi").isMissingNode()); + } + + @Test + void sessionConfigCloneCopiesCapiReference() { + var capi = new CapiSessionOptions().setDisableWebSocketResponses(true); + + var clone = new SessionConfig().setCapi(capi).clone(); + + assertSame(capi, clone.getCapi()); + } + + @Test + void resumeSessionConfigCloneCopiesCapiReference() { + var capi = new CapiSessionOptions().setDisableWebSocketResponses(true); + + var clone = new ResumeSessionConfig().setCapi(capi).clone(); + + assertSame(capi, clone.getCapi()); + } + + @Test + void falseValueIsSerializedWhenExplicitlySet() { + var capi = new CapiSessionOptions().setDisableWebSocketResponses(false); + + JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(capi); + + assertFalse(json.get("disableWebSocketResponses").asBoolean()); + } +} diff --git a/java/src/test/java/com/github/copilot/JsonIncludeNonNullTest.java b/java/src/test/java/com/github/copilot/JsonIncludeNonNullTest.java index b25f573c6..ec7ead567 100644 --- a/java/src/test/java/com/github/copilot/JsonIncludeNonNullTest.java +++ b/java/src/test/java/com/github/copilot/JsonIncludeNonNullTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.Test; +import com.github.copilot.rpc.CapiSessionOptions; import com.github.copilot.rpc.CopilotClientOptions; import com.github.copilot.rpc.CustomAgentConfig; import com.github.copilot.rpc.InfiniteSessionConfig; @@ -76,6 +77,11 @@ void providerConfigHasNonNullAnnotation() { assertHasNonNullInclude(ProviderConfig.class); } + @Test + void capiSessionOptionsHasNonNullAnnotation() { + assertHasNonNullInclude(CapiSessionOptions.class); + } + @Test void telemetryConfigHasNonNullAnnotation() { assertHasNonNullInclude(TelemetryConfig.class); diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 4deda08b4..5e3c829d5 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -1221,6 +1221,7 @@ export class CopilotClient { excludedTools: toolFilterOptions.excludedTools, toolFilterPrecedence: toolFilterOptions.toolFilterPrecedence, provider: config.provider, + capi: config.capi, enableSessionTelemetry: config.enableSessionTelemetry, modelCapabilities: config.modelCapabilities, largeOutput: toWireLargeOutput(config.largeOutput), @@ -1405,6 +1406,7 @@ export class CopilotClient { description: cmd.description, })), provider: config.provider, + capi: config.capi, modelCapabilities: config.modelCapabilities, largeOutput: toWireLargeOutput(config.largeOutput), requestPermission: diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index a7ebbbde0..2ed96e7a0 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -84,6 +84,7 @@ export type { ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext, + CapiSessionOptions, ModelCapabilities, ModelCapabilitiesOverride, ModelInfo, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index bad1c33ad..ad2a19b75 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -1584,6 +1584,31 @@ export interface ExtensionInfo { name: string; } +/** + * Provider-scoped options for the Copilot API (CAPI). + * + * These settings apply to the built-in Copilot API provider only. They live + * under their own namespace because a single session can host multiple + * providers (CAPI alongside BYOK via {@link ProviderConfig}), so transport and + * provider-level choices are conceptually per-provider rather than global. + */ +export interface CapiSessionOptions { + /** + * Opt out of the WebSocket transport for the CAPI Responses API. + * + * WebSocket transport is enabled by default whenever the selected model + * advertises the `ws:/responses` endpoint. Set this to `true` to fall back + * to the HTTP Responses transport instead — useful for users behind proxies + * where WebSocket connections fail. + * + * Equivalent to setting the `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES` + * environment variable. + * + * @default false + */ + disableWebSocketResponses?: boolean; +} + /** * Shared configuration fields used by both {@link SessionConfig} (for * creating a new session) and {@link ResumeSessionConfig} (for resuming @@ -1745,6 +1770,13 @@ export interface SessionConfigBase { */ provider?: ProviderConfig; + /** + * Provider-scoped options for the built-in Copilot API (CAPI), such as + * opting out of the WebSocket Responses transport. See + * {@link CapiSessionOptions}. + */ + capi?: CapiSessionOptions; + /** * Enables or disables internal session telemetry for this session. * When `false`, disables session telemetry. When omitted (the default) or `true`, diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 42c0ff18e..41fae7bee 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -171,6 +171,38 @@ describe("CopilotClient", () => { expect(resumePayload.contextTier).toBe("default"); }); + it("forwards capi options in session.create and session.resume", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + + const session = await client.createSession({ + onPermissionRequest: approveAll, + capi: { disableWebSocketResponses: true }, + }); + await client.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + capi: { disableWebSocketResponses: true }, + }); + + const createPayload = spy.mock.calls.find( + ([method]) => method === "session.create" + )![1] as any; + const resumePayload = spy.mock.calls.find( + ([method]) => method === "session.resume" + )![1] as any; + expect(createPayload.capi).toEqual({ disableWebSocketResponses: true }); + expect(resumePayload.capi).toEqual({ disableWebSocketResponses: true }); + }); + it("forwards pluginDirectories and largeOutput in session.create and session.resume", async () => { const client = new CopilotClient(); await client.start(); diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index ff2562d68..fd21867ab 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -28,6 +28,7 @@ OpenCanvasInstance, ) from .client import ( + CapiSessionOptions, ChildProcessRuntimeConnection, CloudSessionOptions, CloudSessionRepository, @@ -176,6 +177,7 @@ "CanvasHostContext", "CanvasHostContextCapabilities", "CanvasJsonSchema", + "CapiSessionOptions", "ChildProcessRuntimeConnection", "CloudSessionOptions", "CloudSessionRepository", diff --git a/python/copilot/client.py b/python/copilot/client.py index 0dff0e5ab..39f8b1f67 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -133,6 +133,13 @@ class CloudSessionOptions: repository: CloudSessionRepository | None = None +class CapiSessionOptions(TypedDict, total=False): + """CAPI provider-scoped session options.""" + + disable_web_socket_responses: bool + """Opt out of WebSocket Responses transport and use HTTP Responses instead.""" + + def _cloud_session_options_to_dict(options: CloudSessionOptions) -> dict[str, Any]: result: dict[str, Any] = {} if options.repository is not None: @@ -146,6 +153,13 @@ def _cloud_session_options_to_dict(options: CloudSessionOptions) -> dict[str, An return result +def _capi_session_options_to_wire(options: CapiSessionOptions) -> dict[str, Any]: + wire: dict[str, Any] = {} + if "disable_web_socket_responses" in options: + wire["disableWebSocketResponses"] = options["disable_web_socket_responses"] + return wire + + def _validate_session_fs_config(config: SessionFsConfig) -> None: if not config.get("initial_working_directory"): raise ValueError("session_fs.initial_working_directory is required") @@ -1627,6 +1641,7 @@ async def create_session( hooks: SessionHooks | None = None, working_directory: str | None = None, provider: ProviderConfig | None = None, + capi: CapiSessionOptions | None = None, enable_session_telemetry: bool | None = None, skip_custom_instructions: bool | None = None, custom_agents_local_only: bool | None = None, @@ -1709,6 +1724,16 @@ async def create_session( hooks: Lifecycle hooks for the session. working_directory: Working directory for the session. provider: Provider configuration for Azure or custom endpoints. + capi: CAPI provider-scoped options. WebSocket transport is the + default for the CAPI Responses API whenever the model advertises + the ``ws:/responses`` endpoint. Set + ``disable_web_socket_responses=True`` to opt out to the HTTP + Responses transport, which is useful behind proxies where + WebSockets fail. This is equivalent to setting the + ``COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES`` environment + variable. The option is under the ``capi`` namespace because a + single session can host multiple providers (CAPI + BYOK), so + transport choice is provider-level. enable_session_telemetry: Enables or disables internal session telemetry for this session. When False, disables session telemetry. When omitted or True, telemetry is enabled for GitHub-authenticated sessions. When @@ -1916,6 +1941,9 @@ async def create_session( if provider: payload["provider"] = self._convert_provider_to_wire_format(provider) + if capi is not None: + payload["capi"] = _capi_session_options_to_wire(capi) + if enable_session_telemetry is not None: payload["enableSessionTelemetry"] = enable_session_telemetry @@ -2204,6 +2232,7 @@ async def resume_session( hooks: SessionHooks | None = None, working_directory: str | None = None, provider: ProviderConfig | None = None, + capi: CapiSessionOptions | None = None, enable_session_telemetry: bool | None = None, skip_custom_instructions: bool | None = None, custom_agents_local_only: bool | None = None, @@ -2287,6 +2316,16 @@ async def resume_session( hooks: Lifecycle hooks for the session. working_directory: Working directory for the session. provider: Provider configuration for Azure or custom endpoints. + capi: CAPI provider-scoped options. WebSocket transport is the + default for the CAPI Responses API whenever the model advertises + the ``ws:/responses`` endpoint. Set + ``disable_web_socket_responses=True`` to opt out to the HTTP + Responses transport, which is useful behind proxies where + WebSockets fail. This is equivalent to setting the + ``COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES`` environment + variable. The option is under the ``capi`` namespace because a + single session can host multiple providers (CAPI + BYOK), so + transport choice is provider-level. enable_session_telemetry: Enables or disables internal session telemetry for this session. When False, disables session telemetry. When omitted or True, telemetry is enabled for GitHub-authenticated sessions. When @@ -2437,6 +2476,8 @@ async def resume_session( payload["toolFilterPrecedence"] = "excluded" if provider: payload["provider"] = self._convert_provider_to_wire_format(provider) + if capi is not None: + payload["capi"] = _capi_session_options_to_wire(capi) if enable_session_telemetry is not None: payload["enableSessionTelemetry"] = enable_session_telemetry if model_capabilities: diff --git a/python/test_client.py b/python/test_client.py index 6af4450de..2b5e14984 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -10,6 +10,7 @@ import pytest from copilot import ( + CapiSessionOptions, CopilotClient, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext, @@ -240,6 +241,46 @@ async def mock_request(method, params, **kwargs): finally: await client.force_stop() + @pytest.mark.asyncio + async def test_create_and_resume_session_forward_capi_options(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + try: + captured = {} + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method in ("session.create", "session.resume"): + result = {"sessionId": params.get("sessionId") or "session-1"} + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + return {} + + client._client.request = mock_request + create_capi: CapiSessionOptions = {"disable_web_socket_responses": True} + resume_capi: CapiSessionOptions = {"disable_web_socket_responses": False} + + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + capi=create_capi, + ) + await client.resume_session( + session.session_id, + on_permission_request=PermissionHandler.approve_all, + capi=resume_capi, + ) + + assert captured["session.create"]["capi"] == { + "disableWebSocketResponses": True, + } + assert captured["session.resume"]["capi"] == { + "disableWebSocketResponses": False, + } + finally: + await client.force_stop() + @pytest.mark.asyncio async def test_create_and_resume_session_forward_plugin_directories_and_large_output(self): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) diff --git a/rust/src/types.rs b/rust/src/types.rs index c0643ec66..dc8a86312 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -1147,6 +1147,44 @@ impl ProviderConfig { } } +/// Provider-scoped CAPI (Copilot API) session options. +/// +/// WebSocket transport is the default for the CAPI Responses API whenever +/// the model advertises the `ws:/responses` endpoint. Set +/// [`disable_web_socket_responses`](Self::disable_web_socket_responses) to +/// `true` to opt out to the HTTP Responses transport instead, which is useful +/// for users behind proxies where WebSockets fail. +/// +/// This is equivalent to setting the +/// `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES` environment variable. The option +/// is scoped under the `capi` namespace because a single session can host +/// multiple providers, so transport choice is provider-level. +#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct CapiSessionOptions { + /// Opt out of WebSocket transport for CAPI Responses API calls. + /// + /// When `Some(true)`, the runtime uses HTTP Responses transport even if + /// the selected model advertises `ws:/responses`. When unset, the runtime + /// default applies. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub disable_web_socket_responses: Option, +} + +impl CapiSessionOptions { + /// Construct CAPI session options with all fields unset. + pub fn new() -> Self { + Self::default() + } + + /// Opt out of WebSocket transport for CAPI Responses API calls. + pub fn with_disable_web_socket_responses(mut self, disable: bool) -> Self { + self.disable_web_socket_responses = Some(disable); + self + } +} + /// Azure-specific provider options. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -1341,6 +1379,12 @@ pub struct SessionConfig { /// requests through this provider instead of the default Copilot /// routing. pub provider: Option, + /// Provider-scoped CAPI session options. + /// + /// Use this to opt out of the default WebSocket transport for CAPI + /// Responses API calls, equivalent to setting + /// `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES`. + pub capi: Option, /// Enables or disables internal session telemetry for this session. /// /// When `Some(false)`, disables session telemetry. When `None` or @@ -1494,6 +1538,7 @@ impl std::fmt::Debug for SessionConfig { .field("agent", &self.agent) .field("infinite_sessions", &self.infinite_sessions) .field("provider", &self.provider) + .field("capi", &self.capi) .field("enable_session_telemetry", &self.enable_session_telemetry) .field("model_capabilities", &self.model_capabilities) .field("memory", &self.memory) @@ -1594,6 +1639,7 @@ impl Default for SessionConfig { agent: None, infinite_sessions: None, provider: None, + capi: None, enable_session_telemetry: None, model_capabilities: None, memory: None, @@ -1737,6 +1783,7 @@ impl SessionConfig { agent: self.agent, infinite_sessions: self.infinite_sessions, provider: self.provider, + capi: self.capi, enable_session_telemetry: self.enable_session_telemetry, model_capabilities: self.model_capabilities, memory: self.memory, @@ -2154,6 +2201,12 @@ impl SessionConfig { self } + /// Configure provider-scoped CAPI session options. + pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self { + self.capi = Some(capi); + self + } + /// Enable or disable internal session telemetry. /// /// See [`Self::enable_session_telemetry`] for default and BYOK behavior. @@ -2349,6 +2402,12 @@ pub struct ResumeSessionConfig { pub infinite_sessions: Option, /// Re-supply BYOK provider configuration on resume. pub provider: Option, + /// Re-supply provider-scoped CAPI session options on resume. + /// + /// Use this to opt out of the default WebSocket transport for CAPI + /// Responses API calls, equivalent to setting + /// `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES`. + pub capi: Option, /// Enables or disables internal session telemetry for this session. /// /// When `Some(false)`, disables session telemetry. When `None` or @@ -2482,6 +2541,7 @@ impl std::fmt::Debug for ResumeSessionConfig { .field("agent", &self.agent) .field("infinite_sessions", &self.infinite_sessions) .field("provider", &self.provider) + .field("capi", &self.capi) .field("enable_session_telemetry", &self.enable_session_telemetry) .field("model_capabilities", &self.model_capabilities) .field("memory", &self.memory) @@ -2626,6 +2686,7 @@ impl ResumeSessionConfig { agent: self.agent, infinite_sessions: self.infinite_sessions, provider: self.provider, + capi: self.capi, enable_session_telemetry: self.enable_session_telemetry, model_capabilities: self.model_capabilities, memory: self.memory, @@ -2703,6 +2764,7 @@ impl ResumeSessionConfig { agent: None, infinite_sessions: None, provider: None, + capi: None, enable_session_telemetry: None, model_capabilities: None, memory: None, @@ -3093,6 +3155,12 @@ impl ResumeSessionConfig { self } + /// Re-supply provider-scoped CAPI session options on resume. + pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self { + self.capi = Some(capi); + self + } + /// Enable or disable internal session telemetry on resume. /// /// See [`Self::enable_session_telemetry`] for default and BYOK behavior. @@ -4356,11 +4424,12 @@ mod tests { use super::{ AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition, - AttachmentSelectionRange, ConnectionState, CustomAgentConfig, DeliveryMode, ExtensionInfo, - GitHubReferenceType, InfiniteSessionConfig, LargeToolOutputConfig, MemoryConfiguration, - ProviderConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, - SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded, - ToolResultResponse, ensure_attachment_display_names, + AttachmentSelectionRange, CapiSessionOptions, ConnectionState, CustomAgentConfig, + DeliveryMode, ExtensionInfo, GitHubReferenceType, InfiniteSessionConfig, + LargeToolOutputConfig, MemoryConfiguration, ProviderConfig, ReasoningSummary, + ResumeSessionConfig, SessionConfig, SessionEvent, SessionId, SystemMessageConfig, Tool, + ToolBinaryResult, ToolResult, ToolResultExpanded, ToolResultResponse, + ensure_attachment_display_names, }; use crate::generated::session_events::TypedSessionEvent; @@ -4781,6 +4850,7 @@ mod tests { .with_config_directory(PathBuf::from("/tmp/config")) .with_working_directory(PathBuf::from("/tmp/work")) .with_github_token("ghp_test") + .with_capi(CapiSessionOptions::new().with_disable_web_socket_responses(true)) .with_enable_session_telemetry(false) .with_include_sub_agent_streaming_events(false) .with_extension_info(ExtensionInfo::new("github-app", "counter")); @@ -4817,6 +4887,10 @@ mod tests { assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config"))); assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work"))); assert_eq!(cfg.github_token.as_deref(), Some("ghp_test")); + assert_eq!( + cfg.capi, + Some(CapiSessionOptions::new().with_disable_web_socket_responses(true)) + ); assert_eq!(cfg.enable_session_telemetry, Some(false)); assert_eq!(cfg.include_sub_agent_streaming_events, Some(false)); assert_eq!( @@ -4847,6 +4921,7 @@ mod tests { .with_config_directory(PathBuf::from("/tmp/config")) .with_working_directory(PathBuf::from("/tmp/work")) .with_github_token("ghp_test") + .with_capi(CapiSessionOptions::new().with_disable_web_socket_responses(true)) .with_enable_session_telemetry(false) .with_include_sub_agent_streaming_events(true) .with_suppress_resume_event(true) @@ -4883,6 +4958,10 @@ mod tests { assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config"))); assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work"))); assert_eq!(cfg.github_token.as_deref(), Some("ghp_test")); + assert_eq!( + cfg.capi, + Some(CapiSessionOptions::new().with_disable_web_socket_responses(true)) + ); assert_eq!(cfg.enable_session_telemetry, Some(false)); assert_eq!(cfg.include_sub_agent_streaming_events, Some(true)); assert_eq!(cfg.suppress_resume_event, Some(true)); @@ -5061,6 +5140,61 @@ mod tests { assert!(wire_unset.get("maxOutputTokens").is_none()); } + #[test] + fn capi_session_options_builder_composes_and_serializes() { + let cfg = CapiSessionOptions::new().with_disable_web_socket_responses(true); + + assert_eq!(cfg.disable_web_socket_responses, Some(true)); + + let wire = serde_json::to_value(&cfg).unwrap(); + assert_eq!( + wire, + serde_json::json!({ "disableWebSocketResponses": true }) + ); + + let unset = CapiSessionOptions::new(); + let wire_unset = serde_json::to_value(&unset).unwrap(); + assert!(wire_unset.get("disableWebSocketResponses").is_none()); + } + + #[test] + fn session_config_with_capi_serializes() { + let (wire, _) = SessionConfig::default() + .with_capi(CapiSessionOptions::new().with_disable_web_socket_responses(true)) + .into_wire(Some(SessionId::from("capi-create"))) + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!( + json["capi"], + serde_json::json!({ "disableWebSocketResponses": true }) + ); + + let (empty_wire, _) = SessionConfig::default() + .into_wire(Some(SessionId::from("capi-create-unset"))) + .expect("no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("capi").is_none()); + } + + #[test] + fn resume_session_config_with_capi_serializes() { + let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume")) + .with_capi(CapiSessionOptions::new().with_disable_web_socket_responses(true)) + .into_wire() + .expect("no duplicate handlers"); + let json = serde_json::to_value(&wire).unwrap(); + assert_eq!( + json["capi"], + serde_json::json!({ "disableWebSocketResponses": true }) + ); + + let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset")) + .into_wire() + .expect("no duplicate handlers"); + let empty_json = serde_json::to_value(&empty_wire).unwrap(); + assert!(empty_json.get("capi").is_none()); + } + #[test] fn system_message_config_builder_composes() { use std::collections::HashMap; diff --git a/rust/src/wire.rs b/rust/src/wire.rs index 1b58abacd..bf908403a 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -24,7 +24,7 @@ use crate::generated::api_types::{ }; use crate::generated::session_events::ReasoningSummary; use crate::types::{ - CloudSessionOptions, CustomAgentConfig, DefaultAgentConfig, ExtensionInfo, + CapiSessionOptions, CloudSessionOptions, CustomAgentConfig, DefaultAgentConfig, ExtensionInfo, InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, MemoryConfiguration, ProviderConfig, SessionId, SystemMessageConfig, Tool, }; @@ -130,6 +130,8 @@ pub(crate) struct SessionCreateWire { #[serde(skip_serializing_if = "Option::is_none")] pub provider: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub capi: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub enable_session_telemetry: Option, #[serde(skip_serializing_if = "Option::is_none")] pub model_capabilities: Option, @@ -239,6 +241,8 @@ pub(crate) struct SessionResumeWire { #[serde(skip_serializing_if = "Option::is_none")] pub provider: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub capi: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub enable_session_telemetry: Option, #[serde(skip_serializing_if = "Option::is_none")] pub model_capabilities: Option, From 91ec4c8f92aca806e9f8e4d78571a2a742c4b2d1 Mon Sep 17 00:00:00 2001 From: Derek Legenzoff Date: Wed, 17 Jun 2026 22:31:17 -0700 Subject: [PATCH 2/4] Add provider.transport BYOK option Add the BYOK provider `transport` field ("http" | "websockets", default "http") to the hand-written ProviderConfig across all six SDK languages, so BYOK OpenAI-compatible providers can opt into delivering Responses API requests over a persistent WebSocket connection instead of HTTP. SDK-side follow-up to github/copilot-agent-runtime#9557, which adds the runtime `transport` option. The SDK's consumer-facing ProviderConfig is hand-written (not generated from the schema), so the field is added as a pass-through mirroring the existing `wireApi` field, flowing through the already-wired `provider` option on session create and resume. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Types.cs | 9 ++++++ dotnet/test/Unit/SerializationTests.cs | 5 +++- go/client_test.go | 27 ++++++++++++++++++ go/types.go | 4 +++ .../github/copilot/rpc/ProviderConfig.java | 28 +++++++++++++++++++ .../github/copilot/ProviderConfigTest.java | 22 +++++++++++++++ nodejs/src/types.ts | 11 ++++++++ nodejs/test/client.test.ts | 4 +++ python/copilot/client.py | 2 ++ python/copilot/session.py | 5 ++++ python/test_client.py | 2 ++ rust/src/types.rs | 15 ++++++++++ 12 files changed, 133 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index abbdc97b8..ed792def0 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -1991,6 +1991,15 @@ public sealed class ProviderConfig [JsonPropertyName("wireApi")] public string? WireApi { get; set; } + ///

+ /// Transport for OpenAI Responses requests ("http" or "websockets"). Defaults to "http". + /// Set to "websockets" to deliver Responses API requests over a persistent WebSocket + /// connection instead of HTTP. Applies to OpenAI-compatible providers using + /// wireApi: "responses". + /// + [JsonPropertyName("transport")] + public string? Transport { get; set; } + /// /// Base URL of the provider's API endpoint. /// diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index 4a6dc21e5..5dbc53acb 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -27,7 +27,8 @@ public void ProviderConfig_CanSerializeHeaders_WithSdkOptions() ModelId = "gpt-4o", WireModel = "my-finetune-v3", MaxPromptTokens = 100_000, - MaxOutputTokens = 4096 + MaxOutputTokens = 4096, + Transport = "websockets" }; var json = JsonSerializer.Serialize(original, options); @@ -39,6 +40,7 @@ public void ProviderConfig_CanSerializeHeaders_WithSdkOptions() Assert.Equal("my-finetune-v3", root.GetProperty("wireModel").GetString()); Assert.Equal(100_000, root.GetProperty("maxPromptTokens").GetInt32()); Assert.Equal(4096, root.GetProperty("maxOutputTokens").GetInt32()); + Assert.Equal("websockets", root.GetProperty("transport").GetString()); var deserialized = JsonSerializer.Deserialize(json, options); Assert.NotNull(deserialized); @@ -48,6 +50,7 @@ public void ProviderConfig_CanSerializeHeaders_WithSdkOptions() Assert.Equal("my-finetune-v3", deserialized.WireModel); Assert.Equal(100_000, deserialized.MaxPromptTokens); Assert.Equal(4096, deserialized.MaxOutputTokens); + Assert.Equal("websockets", deserialized.Transport); } [Fact] diff --git a/go/client_test.go b/go/client_test.go index a6366c7d1..d82ba36d1 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -1464,6 +1464,33 @@ func TestSessionRequests_Capi(t *testing.T) { }) } +func TestProviderConfig_Transport(t *testing.T) { + t.Run("serializes transport with camelCase key", func(t *testing.T) { + cfg := ProviderConfig{BaseURL: "https://example.com", Transport: "websockets"} + data, err := json.Marshal(cfg) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + if m["transport"] != "websockets" { + t.Errorf("Expected transport=websockets, got %v", m["transport"]) + } + }) + + t.Run("omits transport from JSON when unset", func(t *testing.T) { + cfg := ProviderConfig{BaseURL: "https://example.com"} + data, _ := json.Marshal(cfg) + var m map[string]any + json.Unmarshal(data, &m) + if _, ok := m["transport"]; ok { + t.Error("Expected transport to be omitted when unset") + } + }) +} + func TestResumeSessionRequest_Commands(t *testing.T) { t.Run("forwards commands in session.resume RPC", func(t *testing.T) { req := resumeSessionRequest{ diff --git a/go/types.go b/go/types.go index c3210c447..fb78cb031 100644 --- a/go/types.go +++ b/go/types.go @@ -1511,6 +1511,10 @@ type ProviderConfig struct { Type string `json:"type,omitempty"` // WireAPI is the API format (openai/azure only): "completions" or "responses". Defaults to "completions". WireAPI string `json:"wireApi,omitempty"` + // Transport for OpenAI Responses requests: "http" or "websockets". Defaults to "http". + // Set "websockets" to deliver Responses API requests over a persistent WebSocket + // connection instead of HTTP. Applies to OpenAI-compatible providers using WireAPI "responses". + Transport string `json:"transport,omitempty"` // BaseURL is the API endpoint URL BaseURL string `json:"baseUrl"` // APIKey is the API key. Optional for local providers like Ollama. diff --git a/java/src/main/java/com/github/copilot/rpc/ProviderConfig.java b/java/src/main/java/com/github/copilot/rpc/ProviderConfig.java index 6c9cf379f..8ba492ed9 100644 --- a/java/src/main/java/com/github/copilot/rpc/ProviderConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/ProviderConfig.java @@ -44,6 +44,9 @@ public class ProviderConfig { @JsonProperty("wireApi") private String wireApi; + @JsonProperty("transport") + private String transport; + @JsonProperty("baseUrl") private String baseUrl; @@ -122,6 +125,31 @@ public ProviderConfig setWireApi(String wireApi) { return this; } + /** + * Gets the transport for OpenAI Responses requests. + * + * @return the transport ("http" or "websockets") + */ + public String getTransport() { + return transport; + } + + /** + * Sets the transport for OpenAI Responses requests. + *

+ * Defaults to "http". Set to "websockets" to deliver Responses API requests + * over a persistent WebSocket connection instead of HTTP. Applies to + * OpenAI-compatible providers using {@code wireApi} "responses". + * + * @param transport + * the transport ("http" or "websockets") + * @return this config for method chaining + */ + public ProviderConfig setTransport(String transport) { + this.transport = transport; + return this; + } + /** * Gets the base URL for the API. * diff --git a/java/src/test/java/com/github/copilot/ProviderConfigTest.java b/java/src/test/java/com/github/copilot/ProviderConfigTest.java index 5c40230ec..effb36040 100644 --- a/java/src/test/java/com/github/copilot/ProviderConfigTest.java +++ b/java/src/test/java/com/github/copilot/ProviderConfigTest.java @@ -224,6 +224,28 @@ void testSerializeCustomWireApi() throws Exception { assertEquals("responses", json.get("wireApi").asText()); } + @Test + void testSerializeTransport() throws Exception { + var provider = new ProviderConfig().setType("openai").setBaseUrl("https://custom.example.com").setApiKey("key") + .setWireApi("responses").setTransport("websockets"); + + JsonNode json = MAPPER.valueToTree(provider); + + assertEquals("websockets", json.get("transport").asText()); + + ProviderConfig roundTrip = MAPPER.readValue(MAPPER.writeValueAsString(provider), ProviderConfig.class); + assertEquals("websockets", roundTrip.getTransport()); + } + + @Test + void testTransportOmittedWhenNull() throws Exception { + var provider = new ProviderConfig().setType("openai").setBaseUrl("https://custom.example.com"); + + JsonNode json = MAPPER.valueToTree(provider); + + assertTrue(json.path("transport").isMissingNode()); + } + // ========================================================================= // JSON serialization — all fields populated // ========================================================================= diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index ad2a19b75..dab56fc4e 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -2147,6 +2147,17 @@ export interface ProviderConfig { */ wireApi?: "completions" | "responses"; + /** + * Transport for OpenAI Responses requests. Defaults to "http". + * + * Set to "websockets" to deliver Responses API requests over a persistent + * WebSocket connection instead of HTTP. Useful for long-running, + * tool-call-heavy sessions that benefit from incremental + * `previous_response_id` continuations. Applies to OpenAI-compatible + * providers using `wireApi: "responses"`. + */ + transport?: "http" | "websockets"; + /** * API endpoint URL */ diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 41fae7bee..77f6da934 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -989,6 +989,7 @@ describe("CopilotClient", () => { wireModel: "my-finetune-v3", maxPromptTokens: 100_000, maxOutputTokens: 4096, + transport: "websockets", }, }); @@ -1001,6 +1002,7 @@ describe("CopilotClient", () => { wireModel: "my-finetune-v3", maxPromptTokens: 100_000, maxOutputTokens: 4096, + transport: "websockets", }) ); spy.mockRestore(); @@ -1028,6 +1030,7 @@ describe("CopilotClient", () => { wireModel: "my-finetune-v3", maxPromptTokens: 100_000, maxOutputTokens: 4096, + transport: "websockets", }, }); @@ -1040,6 +1043,7 @@ describe("CopilotClient", () => { wireModel: "my-finetune-v3", maxPromptTokens: 100_000, maxOutputTokens: 4096, + transport: "websockets", }) ); spy.mockRestore(); diff --git a/python/copilot/client.py b/python/copilot/client.py index 39f8b1f67..45d768ce6 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -3179,6 +3179,8 @@ def _convert_provider_to_wire_format( wire_provider["apiKey"] = provider["api_key"] if "wire_api" in provider: wire_provider["wireApi"] = provider["wire_api"] + if "transport" in provider: + wire_provider["transport"] = provider["transport"] if "bearer_token" in provider: wire_provider["bearerToken"] = provider["bearer_token"] if "headers" in provider: diff --git a/python/copilot/session.py b/python/copilot/session.py index 3720af05d..cc6d5e279 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -1071,6 +1071,11 @@ class ProviderConfig(TypedDict, total=False): type: Literal["openai", "azure", "anthropic"] wire_api: Literal["completions", "responses"] + # Transport for OpenAI Responses requests. Defaults to "http". Set + # "websockets" to deliver Responses API requests over a persistent WebSocket + # connection instead of HTTP. Applies to OpenAI-compatible providers using + # wire_api "responses". + transport: Literal["http", "websockets"] base_url: str api_key: str # Bearer token for authentication. Sets the Authorization header directly. diff --git a/python/test_client.py b/python/test_client.py index 2b5e14984..f5bfe9851 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -1125,6 +1125,7 @@ async def mock_request(method, params, **kwargs): "wire_model": "my-finetune-v3", "max_prompt_tokens": 100_000, "max_output_tokens": 4096, + "transport": "websockets", }, ) @@ -1135,6 +1136,7 @@ async def mock_request(method, params, **kwargs): assert provider["wireModel"] == "my-finetune-v3" assert provider["maxPromptTokens"] == 100_000 assert provider["maxOutputTokens"] == 4096 + assert provider["transport"] == "websockets" finally: await client.force_stop() diff --git a/rust/src/types.rs b/rust/src/types.rs index dc8a86312..59f8d617a 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -1027,6 +1027,12 @@ pub struct ProviderConfig { /// Defaults to `"completions"`. #[serde(default, skip_serializing_if = "Option::is_none")] pub wire_api: Option, + /// Transport for OpenAI Responses requests: `"http"` or `"websockets"`. + /// Defaults to `"http"`. Set `"websockets"` to deliver Responses API + /// requests over a persistent WebSocket connection instead of HTTP. + /// Applies to OpenAI-compatible providers using `wire_api` `"responses"`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub transport: Option, /// API endpoint URL. pub base_url: String, /// API key. Optional for local providers like Ollama. @@ -1090,6 +1096,13 @@ impl ProviderConfig { self } + /// Set the transport (`"http"` or `"websockets"`) for OpenAI Responses + /// requests. Defaults to `"http"`. + pub fn with_transport(mut self, transport: impl Into) -> Self { + self.transport = Some(transport.into()); + self + } + /// Set the API key. Optional for local providers like Ollama. pub fn with_api_key(mut self, api_key: impl Into) -> Self { self.api_key = Some(api_key.into()); @@ -5100,6 +5113,7 @@ mod tests { let cfg = ProviderConfig::new("https://api.example.com") .with_provider_type("openai") .with_wire_api("completions") + .with_transport("websockets") .with_api_key("sk-test") .with_bearer_token("bearer-test") .with_headers(headers) @@ -5111,6 +5125,7 @@ mod tests { assert_eq!(cfg.base_url, "https://api.example.com"); assert_eq!(cfg.provider_type.as_deref(), Some("openai")); assert_eq!(cfg.wire_api.as_deref(), Some("completions")); + assert_eq!(cfg.transport.as_deref(), Some("websockets")); assert_eq!(cfg.api_key.as_deref(), Some("sk-test")); assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test")); assert_eq!( From 111b756f303f759b31f22a8a2df229a58ed2d9af Mon Sep 17 00:00:00 2001 From: Derek Legenzoff Date: Mon, 22 Jun 2026 12:14:55 -0700 Subject: [PATCH 3/4] Lead with spelled-out 'Copilot API (CAPI)' on first mention Update the CapiSessionOptions type-level doc summary in each language to lead with the spelled-out 'Copilot API (CAPI)' form on first use, matching the existing docs convention (and the Node SDK, which already did this). Python's class docstring previously never expanded the acronym. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Types.cs | 2 +- go/types.go | 2 +- .../main/java/com/github/copilot/rpc/CapiSessionOptions.java | 2 +- python/copilot/client.py | 2 +- rust/src/types.rs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 0a119c21d..a6209cd6e 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -2068,7 +2068,7 @@ public sealed class ProviderConfig } ///

-/// Provider-scoped options for the CAPI (Copilot API) provider. +/// Provider-scoped options for the Copilot API (CAPI) provider. /// public sealed class CapiSessionOptions { diff --git a/go/types.go b/go/types.go index 3ceeb714b..3788cf34e 100644 --- a/go/types.go +++ b/go/types.go @@ -1572,7 +1572,7 @@ type ProviderConfig struct { MaxOutputTokens int `json:"maxOutputTokens,omitempty"` } -// CapiSessionOptions configures provider-scoped CAPI (Copilot API) session behavior. +// CapiSessionOptions configures provider-scoped Copilot API (CAPI) session behavior. // // WebSocket transport is the default for the CAPI Responses API whenever the // model advertises the ws:/responses endpoint. Set DisableWebSocketResponses to diff --git a/java/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java b/java/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java index f63e65872..841468ff1 100644 --- a/java/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java +++ b/java/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java @@ -8,7 +8,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; /** - * Provider-scoped session options for the CAPI (Copilot API) provider. + * Provider-scoped session options for the Copilot API (CAPI) provider. *

* WebSocket transport is the default for the CAPI Responses API whenever the * model advertises the {@code ws:/responses} endpoint. Setting diff --git a/python/copilot/client.py b/python/copilot/client.py index acb6d050e..c9e77bef3 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -136,7 +136,7 @@ class CloudSessionOptions: class CapiSessionOptions(TypedDict, total=False): - """CAPI provider-scoped session options.""" + """Provider-scoped Copilot API (CAPI) session options.""" disable_web_socket_responses: bool """Opt out of WebSocket Responses transport and use HTTP Responses instead.""" diff --git a/rust/src/types.rs b/rust/src/types.rs index 7208bad20..bb3b8b46f 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -1160,7 +1160,7 @@ impl ProviderConfig { } } -/// Provider-scoped CAPI (Copilot API) session options. +/// Provider-scoped Copilot API (CAPI) session options. /// /// WebSocket transport is the default for the CAPI Responses API whenever /// the model advertises the `ws:/responses` endpoint. Set From 1b6db8bdd0c11efc43390ae821cb2c3af186947a Mon Sep 17 00:00:00 2001 From: Derek Legenzoff Date: Mon, 22 Jun 2026 12:33:02 -0700 Subject: [PATCH 4/4] Invert capi flag to enableWebSocketResponses (default true) Rename the hand-written capi session option from disableWebSocketResponses (default false) to enableWebSocketResponses (default true) across all six language SDKs, matching the inverted shape in runtime PR github/copilot-agent-runtime#10551. Setting it to false forces the HTTP Responses transport, equivalent to the unchanged COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES environment variable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Types.cs | 17 +++---- dotnet/test/Unit/CloneTests.cs | 6 +-- dotnet/test/Unit/SerializationTests.cs | 14 +++--- go/client_test.go | 26 +++++------ go/types.go | 11 ++--- .../copilot/rpc/CapiSessionOptions.java | 38 ++++++++-------- .../copilot/rpc/ResumeSessionConfig.java | 6 +-- .../com/github/copilot/rpc/SessionConfig.java | 6 +-- .../copilot/CapiSessionOptionsTest.java | 32 +++++++------- nodejs/src/types.ts | 12 ++--- nodejs/test/client.test.ts | 8 ++-- python/copilot/client.py | 19 +++++--- python/test_client.py | 8 ++-- rust/src/types.rs | 44 +++++++++---------- 14 files changed, 128 insertions(+), 119 deletions(-) diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index a6209cd6e..06878a727 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -2073,18 +2073,19 @@ public sealed class ProviderConfig public sealed class CapiSessionOptions { ///

- /// When , opts out of the WebSocket transport for the CAPI Responses API - /// and uses the HTTP Responses transport instead. + /// When , forces the HTTP Responses transport for the CAPI Responses API + /// instead of the default WebSocket transport. /// /// /// WebSocket transport is the default for CAPI Responses API requests when the model advertises - /// the ws:/responses endpoint. Set this option for users behind proxies where WebSockets - /// fail. This is equivalent to setting the COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES - /// environment variable. The option is scoped under the capi namespace because a single - /// session can host multiple providers, such as CAPI and BYOK, so transport choice is provider-level. + /// the ws:/responses endpoint. Set this to for users behind proxies + /// where WebSockets fail. Setting it to is equivalent to setting the + /// COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES environment variable. The option is scoped under + /// the capi namespace because a single session can host multiple providers, such as CAPI and + /// BYOK, so transport choice is provider-level. /// - [JsonPropertyName("disableWebSocketResponses")] - public bool? DisableWebSocketResponses { get; set; } + [JsonPropertyName("enableWebSocketResponses")] + public bool? EnableWebSocketResponses { get; set; } } /// diff --git a/dotnet/test/Unit/CloneTests.cs b/dotnet/test/Unit/CloneTests.cs index 1081ea0aa..9df9a200c 100644 --- a/dotnet/test/Unit/CloneTests.cs +++ b/dotnet/test/Unit/CloneTests.cs @@ -82,7 +82,7 @@ public void SessionConfig_Clone_CopiesAllProperties() McpOAuthTokenStorage = McpOAuthTokenStorageMode.Persistent, CustomAgents = [new CustomAgentConfig { Name = "agent1", Model = "claude-haiku-4.5" }], Agent = "agent1", - Capi = new CapiSessionOptions { DisableWebSocketResponses = true }, + Capi = new CapiSessionOptions { EnableWebSocketResponses = false }, Cloud = new CloudSessionOptions { Repository = new CloudSessionRepository @@ -523,7 +523,7 @@ public void SessionConfig_Clone_CopiesCapiOptions() { var original = new SessionConfig { - Capi = new CapiSessionOptions { DisableWebSocketResponses = true }, + Capi = new CapiSessionOptions { EnableWebSocketResponses = false }, }; var clone = original.Clone(); @@ -536,7 +536,7 @@ public void ResumeSessionConfig_Clone_CopiesCapiOptions() { var original = new ResumeSessionConfig { - Capi = new CapiSessionOptions { DisableWebSocketResponses = true }, + Capi = new CapiSessionOptions { EnableWebSocketResponses = false }, }; var clone = original.Clone(); diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index 5dbc53acb..baf71c299 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -54,22 +54,22 @@ public void ProviderConfig_CanSerializeHeaders_WithSdkOptions() } [Fact] - public void CapiSessionOptions_CanSerializeDisableWebSocketResponses_WithSdkOptions() + public void CapiSessionOptions_CanSerializeEnableWebSocketResponses_WithSdkOptions() { var options = GetSerializerOptions(); var original = new CapiSessionOptions { - DisableWebSocketResponses = true + EnableWebSocketResponses = false }; var json = JsonSerializer.Serialize(original, options); using var document = JsonDocument.Parse(json); var root = document.RootElement; - Assert.True(root.GetProperty("disableWebSocketResponses").GetBoolean()); + Assert.False(root.GetProperty("enableWebSocketResponses").GetBoolean()); var deserialized = JsonSerializer.Deserialize(json, options); Assert.NotNull(deserialized); - Assert.True(deserialized.DisableWebSocketResponses); + Assert.False(deserialized.EnableWebSocketResponses); } [Fact] @@ -247,7 +247,7 @@ public void ResumeSessionRequest_CanSerializeInstructionDirectories_WithSdkOptio public void SessionRequests_CanSerializeCapiOptions_WithSdkOptions() { var options = GetSerializerOptions(); - var capi = new CapiSessionOptions { DisableWebSocketResponses = true }; + var capi = new CapiSessionOptions { EnableWebSocketResponses = false }; var createRequestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest"); var createRequest = CreateInternalRequest( @@ -257,7 +257,7 @@ public void SessionRequests_CanSerializeCapiOptions_WithSdkOptions() var createJson = JsonSerializer.Serialize(createRequest, createRequestType, options); using var createDocument = JsonDocument.Parse(createJson); - Assert.True(createDocument.RootElement.GetProperty("capi").GetProperty("disableWebSocketResponses").GetBoolean()); + Assert.False(createDocument.RootElement.GetProperty("capi").GetProperty("enableWebSocketResponses").GetBoolean()); var resumeRequestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest"); var resumeRequest = CreateInternalRequest( @@ -267,7 +267,7 @@ public void SessionRequests_CanSerializeCapiOptions_WithSdkOptions() var resumeJson = JsonSerializer.Serialize(resumeRequest, resumeRequestType, options); using var resumeDocument = JsonDocument.Parse(resumeJson); - Assert.True(resumeDocument.RootElement.GetProperty("capi").GetProperty("disableWebSocketResponses").GetBoolean()); + Assert.False(resumeDocument.RootElement.GetProperty("capi").GetProperty("enableWebSocketResponses").GetBoolean()); } [Fact] diff --git a/go/client_test.go b/go/client_test.go index 4013bf20c..383ee315d 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -226,12 +226,12 @@ func TestClient_ForwardsCapiOptionsToSessionRequests(t *testing.T) { }) _, err := client.CreateSession(t.Context(), &SessionConfig{ - Capi: &CapiSessionOptions{DisableWebSocketResponses: Bool(true)}, + Capi: &CapiSessionOptions{EnableWebSocketResponses: Bool(false)}, }) if err != nil { t.Fatalf("CreateSession failed: %v", err) } - assertCapiDisableWebSocketResponses(t, <-createParams) + assertCapiEnableWebSocketResponses(t, <-createParams) resumeParams := make(chan json.RawMessage, 1) server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { @@ -240,15 +240,15 @@ func TestClient_ForwardsCapiOptionsToSessionRequests(t *testing.T) { }) _, err = client.ResumeSessionWithOptions(t.Context(), "resumed-capi", &ResumeSessionConfig{ - Capi: &CapiSessionOptions{DisableWebSocketResponses: Bool(true)}, + Capi: &CapiSessionOptions{EnableWebSocketResponses: Bool(false)}, }) if err != nil { t.Fatalf("ResumeSessionWithOptions failed: %v", err) } - assertCapiDisableWebSocketResponses(t, <-resumeParams) + assertCapiEnableWebSocketResponses(t, <-resumeParams) } -func assertCapiDisableWebSocketResponses(t *testing.T, params json.RawMessage) { +func assertCapiEnableWebSocketResponses(t *testing.T, params json.RawMessage) { t.Helper() var decoded map[string]any @@ -259,8 +259,8 @@ func assertCapiDisableWebSocketResponses(t *testing.T, params json.RawMessage) { if !ok { t.Fatalf("expected capi object in request params, got %T", decoded["capi"]) } - if capi["disableWebSocketResponses"] != true { - t.Fatalf("expected capi.disableWebSocketResponses=true, got %v", capi["disableWebSocketResponses"]) + if capi["enableWebSocketResponses"] != false { + t.Fatalf("expected capi.enableWebSocketResponses=false, got %v", capi["enableWebSocketResponses"]) } } @@ -1410,7 +1410,7 @@ func TestCreateSessionRequest_Cloud(t *testing.T) { func TestSessionRequests_Capi(t *testing.T) { t.Run("forwards capi options in session.create RPC", func(t *testing.T) { req := createSessionRequest{ - Capi: &CapiSessionOptions{DisableWebSocketResponses: Bool(true)}, + Capi: &CapiSessionOptions{EnableWebSocketResponses: Bool(false)}, } data, err := json.Marshal(req) if err != nil { @@ -1424,15 +1424,15 @@ func TestSessionRequests_Capi(t *testing.T) { if !ok { t.Fatalf("Expected capi to be an object, got %T", m["capi"]) } - if capi["disableWebSocketResponses"] != true { - t.Errorf("Expected disableWebSocketResponses=true, got %v", capi["disableWebSocketResponses"]) + if capi["enableWebSocketResponses"] != false { + t.Errorf("Expected enableWebSocketResponses=false, got %v", capi["enableWebSocketResponses"]) } }) t.Run("forwards capi options in session.resume RPC", func(t *testing.T) { req := resumeSessionRequest{ SessionID: "s1", - Capi: &CapiSessionOptions{DisableWebSocketResponses: Bool(true)}, + Capi: &CapiSessionOptions{EnableWebSocketResponses: Bool(false)}, } data, err := json.Marshal(req) if err != nil { @@ -1446,8 +1446,8 @@ func TestSessionRequests_Capi(t *testing.T) { if !ok { t.Fatalf("Expected capi to be an object, got %T", m["capi"]) } - if capi["disableWebSocketResponses"] != true { - t.Errorf("Expected disableWebSocketResponses=true, got %v", capi["disableWebSocketResponses"]) + if capi["enableWebSocketResponses"] != false { + t.Errorf("Expected enableWebSocketResponses=false, got %v", capi["enableWebSocketResponses"]) } }) diff --git a/go/types.go b/go/types.go index 3788cf34e..0219ce3cd 100644 --- a/go/types.go +++ b/go/types.go @@ -1575,16 +1575,17 @@ type ProviderConfig struct { // CapiSessionOptions configures provider-scoped Copilot API (CAPI) session behavior. // // WebSocket transport is the default for the CAPI Responses API whenever the -// model advertises the ws:/responses endpoint. Set DisableWebSocketResponses to -// Bool(true) to opt out to the HTTP Responses transport, which is useful behind +// model advertises the ws:/responses endpoint. Set EnableWebSocketResponses to +// Bool(false) to force the HTTP Responses transport, which is useful behind // proxies where WebSockets fail. This is equivalent to setting the // COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES environment variable. These options // are provider-scoped under the capi namespace because a single session can host // multiple providers, such as CAPI and BYOK, so transport choice is provider-level. type CapiSessionOptions struct { - // DisableWebSocketResponses opts out of the default WebSocket Responses - // transport and uses HTTP Responses transport when set to Bool(true). - DisableWebSocketResponses *bool `json:"disableWebSocketResponses,omitempty"` + // EnableWebSocketResponses controls whether the CAPI Responses API uses + // WebSocket transport. Enabled by default when the model advertises + // ws:/responses support; set to Bool(false) to force HTTP Responses transport. + EnableWebSocketResponses *bool `json:"enableWebSocketResponses,omitempty"` } // AzureProviderOptions contains Azure-specific provider configuration diff --git a/java/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java b/java/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java index 841468ff1..d94d59f67 100644 --- a/java/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java +++ b/java/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java @@ -12,9 +12,9 @@ *

* WebSocket transport is the default for the CAPI Responses API whenever the * model advertises the {@code ws:/responses} endpoint. Setting - * {@link #setDisableWebSocketResponses(Boolean)} to {@code true} opts out to - * the HTTP Responses transport instead, which is useful for users behind - * proxies where WebSockets fail. This is equivalent to setting the + * {@link #setEnableWebSocketResponses(Boolean)} to {@code false} forces the + * HTTP Responses transport instead, which is useful for users behind proxies + * where WebSockets fail. This is equivalent to setting the * {@code COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES} environment variable. *

* These options are scoped under the {@code capi} namespace because a single @@ -29,35 +29,35 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class CapiSessionOptions { - @JsonProperty("disableWebSocketResponses") - private Boolean disableWebSocketResponses; + @JsonProperty("enableWebSocketResponses") + private Boolean enableWebSocketResponses; /** - * Gets whether CAPI Responses API WebSocket transport is disabled. + * Gets whether CAPI Responses API WebSocket transport is enabled. * - * @return {@code true} to opt out of WebSocket Responses transport, - * {@code false} to explicitly allow it, or {@code null} to use the + * @return {@code false} to force the HTTP Responses transport, {@code true} to + * explicitly use WebSocket transport, or {@code null} to use the * default behavior */ - public Boolean getDisableWebSocketResponses() { - return disableWebSocketResponses; + public Boolean getEnableWebSocketResponses() { + return enableWebSocketResponses; } /** - * Sets whether to disable CAPI Responses API WebSocket transport. + * Sets whether to use CAPI Responses API WebSocket transport. *

* WebSocket transport is the default for the CAPI Responses API whenever the - * model advertises the {@code ws:/responses} endpoint. Set this to {@code true} - * to opt out to the HTTP Responses transport instead, which is useful for users - * behind proxies where WebSockets fail. This is equivalent to setting the - * {@code COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES} environment variable. + * model advertises the {@code ws:/responses} endpoint. Set this to + * {@code false} to force the HTTP Responses transport instead, which is useful + * for users behind proxies where WebSockets fail. This is equivalent to setting + * the {@code COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES} environment variable. * - * @param disableWebSocketResponses - * {@code true} to opt out of WebSocket Responses transport + * @param enableWebSocketResponses + * {@code false} to force the HTTP Responses transport * @return this config for method chaining */ - public CapiSessionOptions setDisableWebSocketResponses(Boolean disableWebSocketResponses) { - this.disableWebSocketResponses = disableWebSocketResponses; + public CapiSessionOptions setEnableWebSocketResponses(Boolean enableWebSocketResponses) { + this.enableWebSocketResponses = enableWebSocketResponses; return this; } } diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index 6c376f4ea..df600b0af 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -270,9 +270,9 @@ public CapiSessionOptions getCapi() { /** * Sets CAPI provider-scoped session options. *

- * Use {@link CapiSessionOptions#setDisableWebSocketResponses(Boolean)} to opt - * out of the default CAPI Responses API WebSocket transport and use HTTP - * Responses transport instead. + * Use {@link CapiSessionOptions#setEnableWebSocketResponses(Boolean)} with + * {@code false} to force the HTTP Responses transport instead of the default + * CAPI Responses API WebSocket transport. * * @param capi * the CAPI session options diff --git a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java index 131c10b23..5567d32ac 100644 --- a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -371,9 +371,9 @@ public CapiSessionOptions getCapi() { /** * Sets CAPI provider-scoped session options. *

- * Use {@link CapiSessionOptions#setDisableWebSocketResponses(Boolean)} to opt - * out of the default CAPI Responses API WebSocket transport and use HTTP - * Responses transport instead. + * Use {@link CapiSessionOptions#setEnableWebSocketResponses(Boolean)} with + * {@code false} to force the HTTP Responses transport instead of the default + * CAPI Responses API WebSocket transport. * * @param capi * the CAPI session options diff --git a/java/src/test/java/com/github/copilot/CapiSessionOptionsTest.java b/java/src/test/java/com/github/copilot/CapiSessionOptionsTest.java index fb27a52dd..17e8f131f 100644 --- a/java/src/test/java/com/github/copilot/CapiSessionOptionsTest.java +++ b/java/src/test/java/com/github/copilot/CapiSessionOptionsTest.java @@ -28,45 +28,45 @@ class CapiSessionOptionsTest { void defaultsAreNull() { var capi = new CapiSessionOptions(); - assertNull(capi.getDisableWebSocketResponses()); + assertNull(capi.getEnableWebSocketResponses()); } @Test void fluentSetterReturnsSameInstance() { var capi = new CapiSessionOptions(); - assertSame(capi, capi.setDisableWebSocketResponses(true)); - assertEquals(Boolean.TRUE, capi.getDisableWebSocketResponses()); + assertSame(capi, capi.setEnableWebSocketResponses(true)); + assertEquals(Boolean.TRUE, capi.getEnableWebSocketResponses()); } @Test - void serializesDisableWebSocketResponses() { - var capi = new CapiSessionOptions().setDisableWebSocketResponses(true); + void serializesEnableWebSocketResponses() { + var capi = new CapiSessionOptions().setEnableWebSocketResponses(true); JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(capi); - assertTrue(json.get("disableWebSocketResponses").asBoolean()); + assertTrue(json.get("enableWebSocketResponses").asBoolean()); } @Test - void omitsUnsetDisableWebSocketResponses() { + void omitsUnsetEnableWebSocketResponses() { var capi = new CapiSessionOptions(); JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(capi); - assertTrue(json.path("disableWebSocketResponses").isMissingNode()); + assertTrue(json.path("enableWebSocketResponses").isMissingNode()); assertEquals(0, json.size()); } @Test void createRequestIncludesCapiWhenSet() { - var config = new SessionConfig().setCapi(new CapiSessionOptions().setDisableWebSocketResponses(true)); + var config = new SessionConfig().setCapi(new CapiSessionOptions().setEnableWebSocketResponses(true)); var request = SessionRequestBuilder.buildCreateRequest(config, "session-1"); JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(request); assertNotNull(request.getCapi()); - assertTrue(json.get("capi").get("disableWebSocketResponses").asBoolean()); + assertTrue(json.get("capi").get("enableWebSocketResponses").asBoolean()); } @Test @@ -82,13 +82,13 @@ void createRequestOmitsCapiWhenUnset() { @Test void resumeRequestIncludesCapiWhenSet() { - var config = new ResumeSessionConfig().setCapi(new CapiSessionOptions().setDisableWebSocketResponses(true)); + var config = new ResumeSessionConfig().setCapi(new CapiSessionOptions().setEnableWebSocketResponses(true)); var request = SessionRequestBuilder.buildResumeRequest("session-1", config); JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(request); assertNotNull(request.getCapi()); - assertTrue(json.get("capi").get("disableWebSocketResponses").asBoolean()); + assertTrue(json.get("capi").get("enableWebSocketResponses").asBoolean()); } @Test @@ -104,7 +104,7 @@ void resumeRequestOmitsCapiWhenUnset() { @Test void sessionConfigCloneCopiesCapiReference() { - var capi = new CapiSessionOptions().setDisableWebSocketResponses(true); + var capi = new CapiSessionOptions().setEnableWebSocketResponses(true); var clone = new SessionConfig().setCapi(capi).clone(); @@ -113,7 +113,7 @@ void sessionConfigCloneCopiesCapiReference() { @Test void resumeSessionConfigCloneCopiesCapiReference() { - var capi = new CapiSessionOptions().setDisableWebSocketResponses(true); + var capi = new CapiSessionOptions().setEnableWebSocketResponses(true); var clone = new ResumeSessionConfig().setCapi(capi).clone(); @@ -122,10 +122,10 @@ void resumeSessionConfigCloneCopiesCapiReference() { @Test void falseValueIsSerializedWhenExplicitlySet() { - var capi = new CapiSessionOptions().setDisableWebSocketResponses(false); + var capi = new CapiSessionOptions().setEnableWebSocketResponses(false); JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(capi); - assertFalse(json.get("disableWebSocketResponses").asBoolean()); + assertFalse(json.get("enableWebSocketResponses").asBoolean()); } } diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 34265c258..452a47517 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -1594,19 +1594,19 @@ export interface ExtensionInfo { */ export interface CapiSessionOptions { /** - * Opt out of the WebSocket transport for the CAPI Responses API. + * Whether to use the WebSocket transport for the CAPI Responses API. * * WebSocket transport is enabled by default whenever the selected model - * advertises the `ws:/responses` endpoint. Set this to `true` to fall back + * advertises the `ws:/responses` endpoint. Set this to `false` to fall back * to the HTTP Responses transport instead — useful for users behind proxies * where WebSocket connections fail. * - * Equivalent to setting the `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES` - * environment variable. + * Setting this to `false` is equivalent to setting the + * `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES` environment variable. * - * @default false + * @default true */ - disableWebSocketResponses?: boolean; + enableWebSocketResponses?: boolean; } /** diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 77f6da934..ef804095f 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -186,11 +186,11 @@ describe("CopilotClient", () => { const session = await client.createSession({ onPermissionRequest: approveAll, - capi: { disableWebSocketResponses: true }, + capi: { enableWebSocketResponses: false }, }); await client.resumeSession(session.sessionId, { onPermissionRequest: approveAll, - capi: { disableWebSocketResponses: true }, + capi: { enableWebSocketResponses: false }, }); const createPayload = spy.mock.calls.find( @@ -199,8 +199,8 @@ describe("CopilotClient", () => { const resumePayload = spy.mock.calls.find( ([method]) => method === "session.resume" )![1] as any; - expect(createPayload.capi).toEqual({ disableWebSocketResponses: true }); - expect(resumePayload.capi).toEqual({ disableWebSocketResponses: true }); + expect(createPayload.capi).toEqual({ enableWebSocketResponses: false }); + expect(resumePayload.capi).toEqual({ enableWebSocketResponses: false }); }); it("forwards pluginDirectories and largeOutput in session.create and session.resume", async () => { diff --git a/python/copilot/client.py b/python/copilot/client.py index c9e77bef3..8ba350adc 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -138,8 +138,15 @@ class CloudSessionOptions: class CapiSessionOptions(TypedDict, total=False): """Provider-scoped Copilot API (CAPI) session options.""" - disable_web_socket_responses: bool - """Opt out of WebSocket Responses transport and use HTTP Responses instead.""" + enable_web_socket_responses: bool + """Whether to use WebSocket transport for the CAPI Responses API. + + Enabled by default when the model advertises ``ws:/responses`` support. Set + to ``False`` to force the HTTP Responses transport instead, which is + equivalent to the ``COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES`` environment + variable and useful in environments where WebSockets are blocked (e.g. + behind a proxy). + """ def _cloud_session_options_to_dict(options: CloudSessionOptions) -> dict[str, Any]: @@ -157,8 +164,8 @@ def _cloud_session_options_to_dict(options: CloudSessionOptions) -> dict[str, An def _capi_session_options_to_wire(options: CapiSessionOptions) -> dict[str, Any]: wire: dict[str, Any] = {} - if "disable_web_socket_responses" in options: - wire["disableWebSocketResponses"] = options["disable_web_socket_responses"] + if "enable_web_socket_responses" in options: + wire["enableWebSocketResponses"] = options["enable_web_socket_responses"] return wire @@ -1731,7 +1738,7 @@ async def create_session( capi: CAPI provider-scoped options. WebSocket transport is the default for the CAPI Responses API whenever the model advertises the ``ws:/responses`` endpoint. Set - ``disable_web_socket_responses=True`` to opt out to the HTTP + ``enable_web_socket_responses=False`` to force the HTTP Responses transport, which is useful behind proxies where WebSockets fail. This is equivalent to setting the ``COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES`` environment @@ -2337,7 +2344,7 @@ async def resume_session( capi: CAPI provider-scoped options. WebSocket transport is the default for the CAPI Responses API whenever the model advertises the ``ws:/responses`` endpoint. Set - ``disable_web_socket_responses=True`` to opt out to the HTTP + ``enable_web_socket_responses=False`` to force the HTTP Responses transport, which is useful behind proxies where WebSockets fail. This is equivalent to setting the ``COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES`` environment diff --git a/python/test_client.py b/python/test_client.py index f5bfe9851..a3b7e85f0 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -259,8 +259,8 @@ async def mock_request(method, params, **kwargs): return {} client._client.request = mock_request - create_capi: CapiSessionOptions = {"disable_web_socket_responses": True} - resume_capi: CapiSessionOptions = {"disable_web_socket_responses": False} + create_capi: CapiSessionOptions = {"enable_web_socket_responses": False} + resume_capi: CapiSessionOptions = {"enable_web_socket_responses": True} session = await client.create_session( on_permission_request=PermissionHandler.approve_all, @@ -273,10 +273,10 @@ async def mock_request(method, params, **kwargs): ) assert captured["session.create"]["capi"] == { - "disableWebSocketResponses": True, + "enableWebSocketResponses": False, } assert captured["session.resume"]["capi"] == { - "disableWebSocketResponses": False, + "enableWebSocketResponses": True, } finally: await client.force_stop() diff --git a/rust/src/types.rs b/rust/src/types.rs index bb3b8b46f..01b18eb52 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -1164,11 +1164,11 @@ impl ProviderConfig { /// /// WebSocket transport is the default for the CAPI Responses API whenever /// the model advertises the `ws:/responses` endpoint. Set -/// [`disable_web_socket_responses`](Self::disable_web_socket_responses) to -/// `true` to opt out to the HTTP Responses transport instead, which is useful +/// [`enable_web_socket_responses`](Self::enable_web_socket_responses) to +/// `false` to force the HTTP Responses transport instead, which is useful /// for users behind proxies where WebSockets fail. /// -/// This is equivalent to setting the +/// Setting it to `false` is equivalent to setting the /// `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES` environment variable. The option /// is scoped under the `capi` namespace because a single session can host /// multiple providers, so transport choice is provider-level. @@ -1176,13 +1176,13 @@ impl ProviderConfig { #[serde(rename_all = "camelCase")] #[non_exhaustive] pub struct CapiSessionOptions { - /// Opt out of WebSocket transport for CAPI Responses API calls. + /// Whether to use WebSocket transport for CAPI Responses API calls. /// - /// When `Some(true)`, the runtime uses HTTP Responses transport even if + /// When `Some(false)`, the runtime uses HTTP Responses transport even if /// the selected model advertises `ws:/responses`. When unset, the runtime - /// default applies. + /// default applies (WebSocket transport when advertised). #[serde(default, skip_serializing_if = "Option::is_none")] - pub disable_web_socket_responses: Option, + pub enable_web_socket_responses: Option, } impl CapiSessionOptions { @@ -1191,9 +1191,9 @@ impl CapiSessionOptions { Self::default() } - /// Opt out of WebSocket transport for CAPI Responses API calls. - pub fn with_disable_web_socket_responses(mut self, disable: bool) -> Self { - self.disable_web_socket_responses = Some(disable); + /// Set whether to use WebSocket transport for CAPI Responses API calls. + pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self { + self.enable_web_socket_responses = Some(enable); self } } @@ -5201,7 +5201,7 @@ mod tests { .with_config_directory(PathBuf::from("/tmp/config")) .with_working_directory(PathBuf::from("/tmp/work")) .with_github_token("ghp_test") - .with_capi(CapiSessionOptions::new().with_disable_web_socket_responses(true)) + .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) .with_enable_session_telemetry(false) .with_include_sub_agent_streaming_events(false) .with_extension_info(ExtensionInfo::new("github-app", "counter")); @@ -5240,7 +5240,7 @@ mod tests { assert_eq!(cfg.github_token.as_deref(), Some("ghp_test")); assert_eq!( cfg.capi, - Some(CapiSessionOptions::new().with_disable_web_socket_responses(true)) + Some(CapiSessionOptions::new().with_enable_web_socket_responses(false)) ); assert_eq!(cfg.enable_session_telemetry, Some(false)); assert_eq!(cfg.include_sub_agent_streaming_events, Some(false)); @@ -5272,7 +5272,7 @@ mod tests { .with_config_directory(PathBuf::from("/tmp/config")) .with_working_directory(PathBuf::from("/tmp/work")) .with_github_token("ghp_test") - .with_capi(CapiSessionOptions::new().with_disable_web_socket_responses(true)) + .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) .with_enable_session_telemetry(false) .with_include_sub_agent_streaming_events(true) .with_suppress_resume_event(true) @@ -5311,7 +5311,7 @@ mod tests { assert_eq!(cfg.github_token.as_deref(), Some("ghp_test")); assert_eq!( cfg.capi, - Some(CapiSessionOptions::new().with_disable_web_socket_responses(true)) + Some(CapiSessionOptions::new().with_enable_web_socket_responses(false)) ); assert_eq!(cfg.enable_session_telemetry, Some(false)); assert_eq!(cfg.include_sub_agent_streaming_events, Some(true)); @@ -5495,31 +5495,31 @@ mod tests { #[test] fn capi_session_options_builder_composes_and_serializes() { - let cfg = CapiSessionOptions::new().with_disable_web_socket_responses(true); + let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false); - assert_eq!(cfg.disable_web_socket_responses, Some(true)); + assert_eq!(cfg.enable_web_socket_responses, Some(false)); let wire = serde_json::to_value(&cfg).unwrap(); assert_eq!( wire, - serde_json::json!({ "disableWebSocketResponses": true }) + serde_json::json!({ "enableWebSocketResponses": false }) ); let unset = CapiSessionOptions::new(); let wire_unset = serde_json::to_value(&unset).unwrap(); - assert!(wire_unset.get("disableWebSocketResponses").is_none()); + assert!(wire_unset.get("enableWebSocketResponses").is_none()); } #[test] fn session_config_with_capi_serializes() { let (wire, _) = SessionConfig::default() - .with_capi(CapiSessionOptions::new().with_disable_web_socket_responses(true)) + .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) .into_wire(Some(SessionId::from("capi-create"))) .expect("no duplicate handlers"); let json = serde_json::to_value(&wire).unwrap(); assert_eq!( json["capi"], - serde_json::json!({ "disableWebSocketResponses": true }) + serde_json::json!({ "enableWebSocketResponses": false }) ); let (empty_wire, _) = SessionConfig::default() @@ -5532,13 +5532,13 @@ mod tests { #[test] fn resume_session_config_with_capi_serializes() { let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume")) - .with_capi(CapiSessionOptions::new().with_disable_web_socket_responses(true)) + .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false)) .into_wire() .expect("no duplicate handlers"); let json = serde_json::to_value(&wire).unwrap(); assert_eq!( json["capi"], - serde_json::json!({ "disableWebSocketResponses": true }) + serde_json::json!({ "enableWebSocketResponses": false }) ); let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))