From 443ba57db17eb0bb8b1e2515f19c6824597638ba Mon Sep 17 00:00:00 2001 From: Devraj Mehta Date: Wed, 18 Feb 2026 15:42:47 -0500 Subject: [PATCH 1/3] feat: add clientName to SessionConfig across all SDKs Add clientName as an optional field to SessionConfig and ResumeSessionConfig in all four SDK languages (Node.js, Python, Go, .NET). This allows SDK consumers to identify their application, which is included in the User-Agent header for API requests. The CLI server protocol already supports clientName on both session.create and session.resume requests, but the SDK types were not exposing it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Client.cs | 4 ++++ dotnet/src/Types.cs | 15 +++++++++++++++ go/client.go | 2 ++ go/types.go | 8 ++++++++ nodejs/src/client.ts | 2 ++ nodejs/src/types.ts | 7 +++++++ python/copilot/client.py | 7 +++++++ python/copilot/types.py | 5 +++++ 8 files changed, 50 insertions(+) diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index f000be805..8ae7ddc99 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -371,6 +371,7 @@ public async Task CreateSessionAsync(SessionConfig? config = nul var request = new CreateSessionRequest( config?.Model, config?.SessionId, + config?.ClientName, config?.ReasoningEffort, config?.Tools?.Select(ToolDefinition.FromAIFunction).ToList(), config?.SystemMessage, @@ -454,6 +455,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes var request = new ResumeSessionRequest( sessionId, + config?.ClientName, config?.Model, config?.ReasoningEffort, config?.Tools?.Select(ToolDefinition.FromAIFunction).ToList(), @@ -1375,6 +1377,7 @@ public static string Escape(string arg) internal record CreateSessionRequest( string? Model, string? SessionId, + string? ClientName, string? ReasoningEffort, List? Tools, SystemMessageConfig? SystemMessage, @@ -1409,6 +1412,7 @@ internal record CreateSessionResponse( internal record ResumeSessionRequest( string SessionId, + string? ClientName, string? Model, string? ReasoningEffort, List? Tools, diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 50e39ff7c..2afdd20f6 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -745,6 +745,7 @@ protected SessionConfig(SessionConfig? other) if (other is null) return; AvailableTools = other.AvailableTools is not null ? [.. other.AvailableTools] : null; + ClientName = other.ClientName; ConfigDir = other.ConfigDir; CustomAgents = other.CustomAgents is not null ? [.. other.CustomAgents] : null; DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null; @@ -768,6 +769,13 @@ protected SessionConfig(SessionConfig? other) } public string? SessionId { get; set; } + + /// + /// Client name to identify the application using the SDK. + /// Included in the User-Agent header for API requests. + /// + public string? ClientName { get; set; } + public string? Model { get; set; } /// @@ -874,6 +882,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) if (other is null) return; AvailableTools = other.AvailableTools is not null ? [.. other.AvailableTools] : null; + ClientName = other.ClientName; ConfigDir = other.ConfigDir; CustomAgents = other.CustomAgents is not null ? [.. other.CustomAgents] : null; DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null; @@ -896,6 +905,12 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) WorkingDirectory = other.WorkingDirectory; } + /// + /// Client name to identify the application using the SDK. + /// Included in the User-Agent header for API requests. + /// + public string? ClientName { get; set; } + /// /// Model to use for this session. Can change the model when resuming. /// diff --git a/go/client.go b/go/client.go index 77a9eeeda..81d9a2060 100644 --- a/go/client.go +++ b/go/client.go @@ -455,6 +455,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses if config != nil { req.Model = config.Model req.SessionID = config.SessionID + req.ClientName = config.ClientName req.ReasoningEffort = config.ReasoningEffort req.ConfigDir = config.ConfigDir req.Tools = config.Tools @@ -552,6 +553,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, var req resumeSessionRequest req.SessionID = sessionID if config != nil { + req.ClientName = config.ClientName req.Model = config.Model req.ReasoningEffort = config.ReasoningEffort req.SystemMessage = config.SystemMessage diff --git a/go/types.go b/go/types.go index b9b649b68..9c6a257ac 100644 --- a/go/types.go +++ b/go/types.go @@ -330,6 +330,9 @@ type InfiniteSessionConfig struct { type SessionConfig struct { // SessionID is an optional custom session ID SessionID string + // ClientName identifies the application using the SDK. + // Included in the User-Agent header for API requests. + ClientName string // Model to use for this session Model string // ReasoningEffort level for models that support it. @@ -409,6 +412,9 @@ type ToolResult struct { // ResumeSessionConfig configures options when resuming a session type ResumeSessionConfig struct { + // ClientName identifies the application using the SDK. + // Included in the User-Agent header for API requests. + ClientName string // Model to use for this session. Can change the model when resuming. Model string // Tools exposes caller-implemented tools to the CLI @@ -626,6 +632,7 @@ type permissionRequestResponse struct { type createSessionRequest struct { Model string `json:"model,omitempty"` SessionID string `json:"sessionId,omitempty"` + ClientName string `json:"clientName,omitempty"` ReasoningEffort string `json:"reasoningEffort,omitempty"` Tools []Tool `json:"tools,omitempty"` SystemMessage *SystemMessageConfig `json:"systemMessage,omitempty"` @@ -655,6 +662,7 @@ type createSessionResponse struct { // resumeSessionRequest is the request for session.resume type resumeSessionRequest struct { SessionID string `json:"sessionId"` + ClientName string `json:"clientName,omitempty"` Model string `json:"model,omitempty"` ReasoningEffort string `json:"reasoningEffort,omitempty"` Tools []Tool `json:"tools,omitempty"` diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 5d7413140..d95e82ccc 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -513,6 +513,7 @@ export class CopilotClient { const response = await this.connection!.sendRequest("session.create", { model: config.model, sessionId: config.sessionId, + clientName: config.clientName, reasoningEffort: config.reasoningEffort, tools: config.tools?.map((tool) => ({ name: tool.name, @@ -594,6 +595,7 @@ export class CopilotClient { const response = await this.connection!.sendRequest("session.resume", { sessionId, + clientName: config.clientName, model: config.model, reasoningEffort: config.reasoningEffort, systemMessage: config.systemMessage, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index c28068043..16971e303 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -615,6 +615,12 @@ export interface SessionConfig { */ sessionId?: string; + /** + * Client name to identify the application using the SDK. + * Included in the User-Agent header for API requests. + */ + clientName?: string; + /** * Model to use for this session */ @@ -730,6 +736,7 @@ export interface SessionConfig { */ export type ResumeSessionConfig = Pick< SessionConfig, + | "clientName" | "model" | "tools" | "systemMessage" diff --git a/python/copilot/client.py b/python/copilot/client.py index 99154f43e..aad272325 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -467,6 +467,8 @@ async def create_session(self, config: Optional[SessionConfig] = None) -> Copilo payload["model"] = cfg["model"] if cfg.get("session_id"): payload["sessionId"] = cfg["session_id"] + if cfg.get("client_name"): + payload["clientName"] = cfg["client_name"] if cfg.get("reasoning_effort"): payload["reasoningEffort"] = cfg["reasoning_effort"] if tool_defs: @@ -629,6 +631,11 @@ async def resume_session( payload: dict[str, Any] = {"sessionId": session_id} + # Add client name if provided + client_name = cfg.get("client_name") + if client_name: + payload["clientName"] = client_name + # Add model if provided model = cfg.get("model") if model: diff --git a/python/copilot/types.py b/python/copilot/types.py index 0f127d445..baa3575fa 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -462,6 +462,9 @@ class SessionConfig(TypedDict, total=False): """Configuration for creating a session""" session_id: str # Optional custom session ID + # Client name to identify the application using the SDK. + # Included in the User-Agent header for API requests. + client_name: str model: str # Model to use for this session. Use client.list_models() to see available models. # Reasoning effort level for models that support it. # Only valid for models where capabilities.supports.reasoning_effort is True. @@ -529,6 +532,8 @@ class ProviderConfig(TypedDict, total=False): class ResumeSessionConfig(TypedDict, total=False): """Configuration for resuming a session""" + # Client name to identify the application using the SDK. + client_name: str # Model to use for this session. Can change the model when resuming. model: str tools: list[Tool] From f764bf335fd9b07dbc8fd08411ed4e9e7701c947 Mon Sep 17 00:00:00 2001 From: Devraj Mehta Date: Wed, 18 Feb 2026 16:03:20 -0500 Subject: [PATCH 2/3] docs: align ResumeSessionConfig.client_name comment with SessionConfig Add the User-Agent header note to the ResumeSessionConfig.client_name comment to be consistent with SessionConfig.client_name. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/copilot/types.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/copilot/types.py b/python/copilot/types.py index baa3575fa..a58051d65 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -533,6 +533,7 @@ class ResumeSessionConfig(TypedDict, total=False): """Configuration for resuming a session""" # Client name to identify the application using the SDK. + # Included in the User-Agent header for API requests. client_name: str # Model to use for this session. Can change the model when resuming. model: str From b770937afa82f171b97bd3e0519ab9accc3fc0cc Mon Sep 17 00:00:00 2001 From: Devraj Mehta Date: Wed, 18 Feb 2026 16:09:40 -0500 Subject: [PATCH 3/3] test: add clientName forwarding tests across all SDKs Add tests verifying clientName is included in session.create and session.resume RPC payloads when set: - Node.js: spy on connection.sendRequest to verify payload - Python: mock _client.request to capture and assert payload - Go: JSON serialization tests for internal request structs - .NET: clone test for ClientName on SessionConfig Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/test/CloneTests.cs | 2 ++ go/client_test.go | 55 ++++++++++++++++++++++++++++++++++++++ nodejs/test/client.test.ts | 31 ++++++++++++++++++++- python/test_client.py | 42 +++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 1 deletion(-) diff --git a/dotnet/test/CloneTests.cs b/dotnet/test/CloneTests.cs index 10ad02054..45eaaae16 100644 --- a/dotnet/test/CloneTests.cs +++ b/dotnet/test/CloneTests.cs @@ -78,6 +78,7 @@ public void SessionConfig_Clone_CopiesAllProperties() var original = new SessionConfig { SessionId = "test-session", + ClientName = "my-app", Model = "gpt-4", ReasoningEffort = "high", ConfigDir = "/config", @@ -94,6 +95,7 @@ public void SessionConfig_Clone_CopiesAllProperties() var clone = original.Clone(); Assert.Equal(original.SessionId, clone.SessionId); + Assert.Equal(original.ClientName, clone.ClientName); Assert.Equal(original.Model, clone.Model); Assert.Equal(original.ReasoningEffort, clone.ReasoningEffort); Assert.Equal(original.ConfigDir, clone.ConfigDir); diff --git a/go/client_test.go b/go/client_test.go index 176dad8c5..b2e9cdce6 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -1,6 +1,7 @@ package copilot import ( + "encoding/json" "os" "path/filepath" "reflect" @@ -389,3 +390,57 @@ func fileExistsForTest(path string) bool { _, err := os.Stat(path) return err == nil } + +func TestCreateSessionRequest_ClientName(t *testing.T) { + t.Run("includes clientName in JSON when set", func(t *testing.T) { + req := createSessionRequest{ClientName: "my-app"} + 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) + } + if m["clientName"] != "my-app" { + t.Errorf("Expected clientName to be 'my-app', got %v", m["clientName"]) + } + }) + + t.Run("omits clientName from JSON when empty", func(t *testing.T) { + req := createSessionRequest{} + data, _ := json.Marshal(req) + var m map[string]any + json.Unmarshal(data, &m) + if _, ok := m["clientName"]; ok { + t.Error("Expected clientName to be omitted when empty") + } + }) +} + +func TestResumeSessionRequest_ClientName(t *testing.T) { + t.Run("includes clientName in JSON when set", func(t *testing.T) { + req := resumeSessionRequest{SessionID: "s1", ClientName: "my-app"} + 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) + } + if m["clientName"] != "my-app" { + t.Errorf("Expected clientName to be 'my-app', got %v", m["clientName"]) + } + }) + + t.Run("omits clientName from JSON when empty", func(t *testing.T) { + req := resumeSessionRequest{SessionID: "s1"} + data, _ := json.Marshal(req) + var m map[string]any + json.Unmarshal(data, &m) + if _, ok := m["clientName"]; ok { + t.Error("Expected clientName to be omitted when empty") + } + }) +} diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 4b9b512cb..5d1ed8ac3 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { describe, expect, it, onTestFinished } from "vitest"; +import { describe, expect, it, onTestFinished, vi } from "vitest"; import { CopilotClient } from "../src/index.js"; // This file is for unit tests. Where relevant, prefer to add e2e tests in e2e/*.test.ts instead @@ -27,6 +27,35 @@ describe("CopilotClient", () => { }); }); + it("forwards clientName in session.create request", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const spy = vi.spyOn((client as any).connection!, "sendRequest"); + await client.createSession({ clientName: "my-app" }); + + expect(spy).toHaveBeenCalledWith( + "session.create", + expect.objectContaining({ clientName: "my-app" }) + ); + }); + + it("forwards clientName in session.resume request", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const session = await client.createSession(); + const spy = vi.spyOn((client as any).connection!, "sendRequest"); + await client.resumeSession(session.sessionId, { clientName: "my-app" }); + + expect(spy).toHaveBeenCalledWith( + "session.resume", + expect.objectContaining({ clientName: "my-app", sessionId: session.sessionId }) + ); + }); + describe("URL parsing", () => { it("should parse port-only URL format", () => { const client = new CopilotClient({ diff --git a/python/test_client.py b/python/test_client.py index 7b4af8c0f..0bc99ea69 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -147,3 +147,45 @@ def test_use_logged_in_user_with_cli_url_raises(self): CopilotClient( {"cli_url": "localhost:8080", "use_logged_in_user": False, "log_level": "error"} ) + + +class TestSessionConfigForwarding: + @pytest.mark.asyncio + async def test_create_session_forwards_client_name(self): + client = CopilotClient({"cli_path": CLI_PATH}) + await client.start() + + try: + captured = {} + original_request = client._client.request + + async def mock_request(method, params): + captured[method] = params + return await original_request(method, params) + + client._client.request = mock_request + await client.create_session({"client_name": "my-app"}) + assert captured["session.create"]["clientName"] == "my-app" + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_resume_session_forwards_client_name(self): + client = CopilotClient({"cli_path": CLI_PATH}) + await client.start() + + try: + session = await client.create_session() + + captured = {} + original_request = client._client.request + + async def mock_request(method, params): + captured[method] = params + return await original_request(method, params) + + client._client.request = mock_request + await client.resume_session(session.session_id, {"client_name": "my-app"}) + assert captured["session.resume"]["clientName"] == "my-app" + finally: + await client.force_stop()