Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig? config = nul
var request = new CreateSessionRequest(
config?.Model,
config?.SessionId,
config?.ClientName,
config?.ReasoningEffort,
config?.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
config?.SystemMessage,
Expand Down Expand Up @@ -454,6 +455,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes

var request = new ResumeSessionRequest(
sessionId,
config?.ClientName,
config?.Model,
config?.ReasoningEffort,
config?.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
Expand Down Expand Up @@ -1375,6 +1377,7 @@ public static string Escape(string arg)
internal record CreateSessionRequest(
string? Model,
string? SessionId,
string? ClientName,
string? ReasoningEffort,
List<ToolDefinition>? Tools,
SystemMessageConfig? SystemMessage,
Expand Down Expand Up @@ -1409,6 +1412,7 @@ internal record CreateSessionResponse(

internal record ResumeSessionRequest(
string SessionId,
string? ClientName,
string? Model,
string? ReasoningEffort,
List<ToolDefinition>? Tools,
Expand Down
15 changes: 15 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -768,6 +769,13 @@ protected SessionConfig(SessionConfig? other)
}

public string? SessionId { get; set; }

/// <summary>
/// Client name to identify the application using the SDK.
/// Included in the User-Agent header for API requests.
/// </summary>
public string? ClientName { get; set; }

public string? Model { get; set; }

/// <summary>
Expand Down Expand Up @@ -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;
Expand All @@ -896,6 +905,12 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
WorkingDirectory = other.WorkingDirectory;
}

/// <summary>
/// Client name to identify the application using the SDK.
/// Included in the User-Agent header for API requests.
/// </summary>
public string? ClientName { get; set; }

/// <summary>
/// Model to use for this session. Can change the model when resuming.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions dotnet/test/CloneTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
55 changes: 55 additions & 0 deletions go/client_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package copilot

import (
"encoding/json"
"os"
"path/filepath"
"reflect"
Expand Down Expand Up @@ -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")
}
})
}
8 changes: 8 additions & 0 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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"`
Expand Down
2 changes: 2 additions & 0 deletions nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions nodejs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -730,6 +736,7 @@ export interface SessionConfig {
*/
export type ResumeSessionConfig = Pick<
SessionConfig,
| "clientName"
| "model"
| "tools"
| "systemMessage"
Expand Down
31 changes: 30 additions & 1 deletion nodejs/test/client.test.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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({
Expand Down
7 changes: 7 additions & 0 deletions python/copilot/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions python/copilot/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -529,6 +532,9 @@ class ProviderConfig(TypedDict, total=False):
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
tools: list[Tool]
Expand Down
42 changes: 42 additions & 0 deletions python/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading