Skip to content

Commit 397ef66

Browse files
devm33Copilot
andauthored
feat: add clientName to SessionConfig across all SDKs (#510)
* 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> * 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> * 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> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3e2d2b2 commit 397ef66

File tree

12 files changed

+180
-1
lines changed

12 files changed

+180
-1
lines changed

dotnet/src/Client.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig? config = nul
371371
var request = new CreateSessionRequest(
372372
config?.Model,
373373
config?.SessionId,
374+
config?.ClientName,
374375
config?.ReasoningEffort,
375376
config?.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
376377
config?.SystemMessage,
@@ -454,6 +455,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
454455

455456
var request = new ResumeSessionRequest(
456457
sessionId,
458+
config?.ClientName,
457459
config?.Model,
458460
config?.ReasoningEffort,
459461
config?.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
@@ -1375,6 +1377,7 @@ public static string Escape(string arg)
13751377
internal record CreateSessionRequest(
13761378
string? Model,
13771379
string? SessionId,
1380+
string? ClientName,
13781381
string? ReasoningEffort,
13791382
List<ToolDefinition>? Tools,
13801383
SystemMessageConfig? SystemMessage,
@@ -1409,6 +1412,7 @@ internal record CreateSessionResponse(
14091412

14101413
internal record ResumeSessionRequest(
14111414
string SessionId,
1415+
string? ClientName,
14121416
string? Model,
14131417
string? ReasoningEffort,
14141418
List<ToolDefinition>? Tools,

dotnet/src/Types.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,7 @@ protected SessionConfig(SessionConfig? other)
745745
if (other is null) return;
746746

747747
AvailableTools = other.AvailableTools is not null ? [.. other.AvailableTools] : null;
748+
ClientName = other.ClientName;
748749
ConfigDir = other.ConfigDir;
749750
CustomAgents = other.CustomAgents is not null ? [.. other.CustomAgents] : null;
750751
DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null;
@@ -768,6 +769,13 @@ protected SessionConfig(SessionConfig? other)
768769
}
769770

770771
public string? SessionId { get; set; }
772+
773+
/// <summary>
774+
/// Client name to identify the application using the SDK.
775+
/// Included in the User-Agent header for API requests.
776+
/// </summary>
777+
public string? ClientName { get; set; }
778+
771779
public string? Model { get; set; }
772780

773781
/// <summary>
@@ -874,6 +882,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
874882
if (other is null) return;
875883

876884
AvailableTools = other.AvailableTools is not null ? [.. other.AvailableTools] : null;
885+
ClientName = other.ClientName;
877886
ConfigDir = other.ConfigDir;
878887
CustomAgents = other.CustomAgents is not null ? [.. other.CustomAgents] : null;
879888
DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null;
@@ -896,6 +905,12 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
896905
WorkingDirectory = other.WorkingDirectory;
897906
}
898907

908+
/// <summary>
909+
/// Client name to identify the application using the SDK.
910+
/// Included in the User-Agent header for API requests.
911+
/// </summary>
912+
public string? ClientName { get; set; }
913+
899914
/// <summary>
900915
/// Model to use for this session. Can change the model when resuming.
901916
/// </summary>

dotnet/test/CloneTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
7878
var original = new SessionConfig
7979
{
8080
SessionId = "test-session",
81+
ClientName = "my-app",
8182
Model = "gpt-4",
8283
ReasoningEffort = "high",
8384
ConfigDir = "/config",
@@ -94,6 +95,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
9495
var clone = original.Clone();
9596

9697
Assert.Equal(original.SessionId, clone.SessionId);
98+
Assert.Equal(original.ClientName, clone.ClientName);
9799
Assert.Equal(original.Model, clone.Model);
98100
Assert.Equal(original.ReasoningEffort, clone.ReasoningEffort);
99101
Assert.Equal(original.ConfigDir, clone.ConfigDir);

go/client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
455455
if config != nil {
456456
req.Model = config.Model
457457
req.SessionID = config.SessionID
458+
req.ClientName = config.ClientName
458459
req.ReasoningEffort = config.ReasoningEffort
459460
req.ConfigDir = config.ConfigDir
460461
req.Tools = config.Tools
@@ -550,6 +551,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
550551
var req resumeSessionRequest
551552
req.SessionID = sessionID
552553
if config != nil {
554+
req.ClientName = config.ClientName
553555
req.Model = config.Model
554556
req.ReasoningEffort = config.ReasoningEffort
555557
req.SystemMessage = config.SystemMessage

go/client_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package copilot
22

33
import (
4+
"encoding/json"
45
"os"
56
"path/filepath"
67
"reflect"
@@ -389,3 +390,57 @@ func fileExistsForTest(path string) bool {
389390
_, err := os.Stat(path)
390391
return err == nil
391392
}
393+
394+
func TestCreateSessionRequest_ClientName(t *testing.T) {
395+
t.Run("includes clientName in JSON when set", func(t *testing.T) {
396+
req := createSessionRequest{ClientName: "my-app"}
397+
data, err := json.Marshal(req)
398+
if err != nil {
399+
t.Fatalf("Failed to marshal: %v", err)
400+
}
401+
var m map[string]any
402+
if err := json.Unmarshal(data, &m); err != nil {
403+
t.Fatalf("Failed to unmarshal: %v", err)
404+
}
405+
if m["clientName"] != "my-app" {
406+
t.Errorf("Expected clientName to be 'my-app', got %v", m["clientName"])
407+
}
408+
})
409+
410+
t.Run("omits clientName from JSON when empty", func(t *testing.T) {
411+
req := createSessionRequest{}
412+
data, _ := json.Marshal(req)
413+
var m map[string]any
414+
json.Unmarshal(data, &m)
415+
if _, ok := m["clientName"]; ok {
416+
t.Error("Expected clientName to be omitted when empty")
417+
}
418+
})
419+
}
420+
421+
func TestResumeSessionRequest_ClientName(t *testing.T) {
422+
t.Run("includes clientName in JSON when set", func(t *testing.T) {
423+
req := resumeSessionRequest{SessionID: "s1", ClientName: "my-app"}
424+
data, err := json.Marshal(req)
425+
if err != nil {
426+
t.Fatalf("Failed to marshal: %v", err)
427+
}
428+
var m map[string]any
429+
if err := json.Unmarshal(data, &m); err != nil {
430+
t.Fatalf("Failed to unmarshal: %v", err)
431+
}
432+
if m["clientName"] != "my-app" {
433+
t.Errorf("Expected clientName to be 'my-app', got %v", m["clientName"])
434+
}
435+
})
436+
437+
t.Run("omits clientName from JSON when empty", func(t *testing.T) {
438+
req := resumeSessionRequest{SessionID: "s1"}
439+
data, _ := json.Marshal(req)
440+
var m map[string]any
441+
json.Unmarshal(data, &m)
442+
if _, ok := m["clientName"]; ok {
443+
t.Error("Expected clientName to be omitted when empty")
444+
}
445+
})
446+
}

go/types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,9 @@ type InfiniteSessionConfig struct {
330330
type SessionConfig struct {
331331
// SessionID is an optional custom session ID
332332
SessionID string
333+
// ClientName identifies the application using the SDK.
334+
// Included in the User-Agent header for API requests.
335+
ClientName string
333336
// Model to use for this session
334337
Model string
335338
// ReasoningEffort level for models that support it.
@@ -411,6 +414,9 @@ type ToolResult struct {
411414

412415
// ResumeSessionConfig configures options when resuming a session
413416
type ResumeSessionConfig struct {
417+
// ClientName identifies the application using the SDK.
418+
// Included in the User-Agent header for API requests.
419+
ClientName string
414420
// Model to use for this session. Can change the model when resuming.
415421
Model string
416422
// Tools exposes caller-implemented tools to the CLI
@@ -630,6 +636,7 @@ type permissionRequestResponse struct {
630636
type createSessionRequest struct {
631637
Model string `json:"model,omitempty"`
632638
SessionID string `json:"sessionId,omitempty"`
639+
ClientName string `json:"clientName,omitempty"`
633640
ReasoningEffort string `json:"reasoningEffort,omitempty"`
634641
Tools []Tool `json:"tools,omitempty"`
635642
SystemMessage *SystemMessageConfig `json:"systemMessage,omitempty"`
@@ -659,6 +666,7 @@ type createSessionResponse struct {
659666
// resumeSessionRequest is the request for session.resume
660667
type resumeSessionRequest struct {
661668
SessionID string `json:"sessionId"`
669+
ClientName string `json:"clientName,omitempty"`
662670
Model string `json:"model,omitempty"`
663671
ReasoningEffort string `json:"reasoningEffort,omitempty"`
664672
Tools []Tool `json:"tools,omitempty"`

nodejs/src/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,7 @@ export class CopilotClient {
513513
const response = await this.connection!.sendRequest("session.create", {
514514
model: config.model,
515515
sessionId: config.sessionId,
516+
clientName: config.clientName,
516517
reasoningEffort: config.reasoningEffort,
517518
tools: config.tools?.map((tool) => ({
518519
name: tool.name,
@@ -594,6 +595,7 @@ export class CopilotClient {
594595

595596
const response = await this.connection!.sendRequest("session.resume", {
596597
sessionId,
598+
clientName: config.clientName,
597599
model: config.model,
598600
reasoningEffort: config.reasoningEffort,
599601
systemMessage: config.systemMessage,

nodejs/src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,12 @@ export interface SessionConfig {
617617
*/
618618
sessionId?: string;
619619

620+
/**
621+
* Client name to identify the application using the SDK.
622+
* Included in the User-Agent header for API requests.
623+
*/
624+
clientName?: string;
625+
620626
/**
621627
* Model to use for this session
622628
*/
@@ -732,6 +738,7 @@ export interface SessionConfig {
732738
*/
733739
export type ResumeSessionConfig = Pick<
734740
SessionConfig,
741+
| "clientName"
735742
| "model"
736743
| "tools"
737744
| "systemMessage"

nodejs/test/client.test.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
import { describe, expect, it, onTestFinished } from "vitest";
2+
import { describe, expect, it, onTestFinished, vi } from "vitest";
33
import { CopilotClient } from "../src/index.js";
44

55
// This file is for unit tests. Where relevant, prefer to add e2e tests in e2e/*.test.ts instead
@@ -27,6 +27,35 @@ describe("CopilotClient", () => {
2727
});
2828
});
2929

30+
it("forwards clientName in session.create request", async () => {
31+
const client = new CopilotClient();
32+
await client.start();
33+
onTestFinished(() => client.forceStop());
34+
35+
const spy = vi.spyOn((client as any).connection!, "sendRequest");
36+
await client.createSession({ clientName: "my-app" });
37+
38+
expect(spy).toHaveBeenCalledWith(
39+
"session.create",
40+
expect.objectContaining({ clientName: "my-app" })
41+
);
42+
});
43+
44+
it("forwards clientName in session.resume request", async () => {
45+
const client = new CopilotClient();
46+
await client.start();
47+
onTestFinished(() => client.forceStop());
48+
49+
const session = await client.createSession();
50+
const spy = vi.spyOn((client as any).connection!, "sendRequest");
51+
await client.resumeSession(session.sessionId, { clientName: "my-app" });
52+
53+
expect(spy).toHaveBeenCalledWith(
54+
"session.resume",
55+
expect.objectContaining({ clientName: "my-app", sessionId: session.sessionId })
56+
);
57+
});
58+
3059
describe("URL parsing", () => {
3160
it("should parse port-only URL format", () => {
3261
const client = new CopilotClient({

python/copilot/client.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,8 @@ async def create_session(self, config: Optional[SessionConfig] = None) -> Copilo
467467
payload["model"] = cfg["model"]
468468
if cfg.get("session_id"):
469469
payload["sessionId"] = cfg["session_id"]
470+
if cfg.get("client_name"):
471+
payload["clientName"] = cfg["client_name"]
470472
if cfg.get("reasoning_effort"):
471473
payload["reasoningEffort"] = cfg["reasoning_effort"]
472474
if tool_defs:
@@ -628,6 +630,11 @@ async def resume_session(
628630

629631
payload: dict[str, Any] = {"sessionId": session_id}
630632

633+
# Add client name if provided
634+
client_name = cfg.get("client_name")
635+
if client_name:
636+
payload["clientName"] = client_name
637+
631638
# Add model if provided
632639
model = cfg.get("model")
633640
if model:

0 commit comments

Comments
 (0)