Skip to content
Draft
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
7 changes: 5 additions & 2 deletions dotnet/src/Generated/Rpc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,9 @@ internal class SessionModelSwitchToRequest

[JsonPropertyName("modelId")]
public string ModelId { get; set; } = string.Empty;

[JsonPropertyName("reasoningEffort")]
public string? ReasoningEffort { get; set; }
}

public class SessionModeGetResult
Expand Down Expand Up @@ -615,9 +618,9 @@ public async Task<SessionModelGetCurrentResult> GetCurrentAsync(CancellationToke
}

/// <summary>Calls "session.model.switchTo".</summary>
public async Task<SessionModelSwitchToResult> SwitchToAsync(string modelId, CancellationToken cancellationToken = default)
public async Task<SessionModelSwitchToResult> SwitchToAsync(string modelId, string? reasoningEffort = null, CancellationToken cancellationToken = default)
{
var request = new SessionModelSwitchToRequest { SessionId = _sessionId, ModelId = modelId };
var request = new SessionModelSwitchToRequest { SessionId = _sessionId, ModelId = modelId, ReasoningEffort = reasoningEffort };
return await CopilotClient.InvokeRpcAsync<SessionModelSwitchToResult>(_rpc, "session.model.switchTo", [request], cancellationToken);
}
}
Expand Down
6 changes: 4 additions & 2 deletions dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -510,15 +510,17 @@ await InvokeRpcAsync<object>(
/// The new model takes effect for the next message. Conversation history is preserved.
/// </summary>
/// <param name="model">Model ID to switch to (e.g., "gpt-4.1").</param>
/// <param name="reasoningEffort">Optional reasoning effort level for models that support it (e.g., "low", "medium", "high", "xhigh").</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <example>
/// <code>
/// await session.SetModelAsync("gpt-4.1");
/// await session.SetModelAsync("gpt-4.1", reasoningEffort: "high");
/// </code>
/// </example>
public async Task SetModelAsync(string model, CancellationToken cancellationToken = default)
public async Task SetModelAsync(string model, string? reasoningEffort = null, CancellationToken cancellationToken = default)
{
await Rpc.Model.SwitchToAsync(model, cancellationToken);
await Rpc.Model.SwitchToAsync(model, reasoningEffort, cancellationToken);
}

/// <summary>
Expand Down
15 changes: 15 additions & 0 deletions dotnet/test/SessionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -404,4 +404,19 @@ public async Task Should_Set_Model_On_Existing_Session()
var modelChanged = await modelChangedTask;
Assert.Equal("gpt-4.1", modelChanged.Data.NewModel);
}

[Fact]
public async Task Should_Set_Model_With_Reasoning_Effort_On_Existing_Session()
{
var session = await CreateSessionAsync();

// Subscribe for the model change event before calling SetModelAsync
var modelChangedTask = TestHelper.GetNextEventOfTypeAsync<SessionModelChangeEvent>(session);

await session.SetModelAsync("gpt-4.1", reasoningEffort: "high");

// Verify a model_change event was emitted with the new model
var modelChanged = await modelChangedTask;
Assert.Equal("gpt-4.1", modelChanged.Data.NewModel);
}
}
19 changes: 18 additions & 1 deletion go/internal/e2e/rpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,11 +201,28 @@ func TestSessionRpc(t *testing.T) {
t.Fatalf("Failed to create session: %v", err)
}

if err := session.SetModel(t.Context(), "gpt-4.1"); err != nil {
if err := session.SetModel(t.Context(), "gpt-4.1", nil); err != nil {
t.Fatalf("SetModel returned error: %v", err)
}
})

t.Run("should call session.SetModel with reasoning effort", func(t *testing.T) {
t.Skip("session.model.switchTo not yet implemented in CLI")

session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
Model: "claude-sonnet-4.5",
})
if err != nil {
t.Fatalf("Failed to create session: %v", err)
}

effort := "high"
if err := session.SetModel(t.Context(), "gpt-4.1", &effort); err != nil {
t.Fatalf("SetModel with reasoning effort returned error: %v", err)
}
})

t.Run("should get and set session mode", func(t *testing.T) {
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll})
if err != nil {
Expand Down
6 changes: 5 additions & 1 deletion go/rpc/generated_rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ type SessionModelSwitchToResult struct {
}

type SessionModelSwitchToParams struct {
ModelID string `json:"modelId"`
ModelID string `json:"modelId"`
ReasoningEffort *string `json:"reasoningEffort,omitempty"`
}

type SessionModeGetResult struct {
Expand Down Expand Up @@ -365,6 +366,9 @@ func (a *ModelRpcApi) SwitchTo(ctx context.Context, params *SessionModelSwitchTo
req := map[string]interface{}{"sessionId": a.sessionID}
if params != nil {
req["modelId"] = params.ModelID
if params.ReasoningEffort != nil {
req["reasoningEffort"] = *params.ReasoningEffort
}
}
raw, err := a.client.Request("session.model.switchTo", req)
if err != nil {
Expand Down
13 changes: 10 additions & 3 deletions go/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -579,14 +579,21 @@ func (s *Session) Abort(ctx context.Context) error {

// SetModel changes the model for this session.
// The new model takes effect for the next message. Conversation history is preserved.
// The optional reasoningEffort parameter sets the reasoning effort level for models that support it
// (e.g., "low", "medium", "high", "xhigh"). Pass nil to omit it.
//
// Example:
//
// if err := session.SetModel(context.Background(), "gpt-4.1"); err != nil {
// if err := session.SetModel(context.Background(), "gpt-4.1", nil); err != nil {
// log.Printf("Failed to set model: %v", err)
// }
func (s *Session) SetModel(ctx context.Context, model string) error {
_, err := s.RPC.Model.SwitchTo(ctx, &rpc.SessionModelSwitchToParams{ModelID: model})
//
// effort := "high"
// if err := session.SetModel(context.Background(), "gpt-4.1", &effort); err != nil {
// log.Printf("Failed to set model: %v", err)
// }
func (s *Session) SetModel(ctx context.Context, model string, reasoningEffort *string) error {
_, err := s.RPC.Model.SwitchTo(ctx, &rpc.SessionModelSwitchToParams{ModelID: model, ReasoningEffort: reasoningEffort})
if err != nil {
return fmt.Errorf("failed to set model: %w", err)
}
Expand Down
1 change: 1 addition & 0 deletions nodejs/src/generated/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ export interface SessionModelSwitchToParams {
*/
sessionId: string;
modelId: string;
reasoningEffort?: string;
}

export interface SessionModeGetResult {
Expand Down
7 changes: 5 additions & 2 deletions nodejs/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
PermissionHandler,
PermissionRequest,
PermissionRequestResult,
ReasoningEffort,
SessionEvent,
SessionEventHandler,
SessionEventPayload,
Expand Down Expand Up @@ -555,13 +556,15 @@ export class CopilotSession {
* The new model takes effect for the next message. Conversation history is preserved.
*
* @param model - Model ID to switch to
* @param reasoningEffort - Optional reasoning effort level (e.g., "low", "medium", "high", "xhigh")
*
* @example
* ```typescript
* await session.setModel("gpt-4.1");
* await session.setModel("gpt-4.1", "high");
* ```
*/
async setModel(model: string): Promise<void> {
await this.rpc.model.switchTo({ modelId: model });
async setModel(model: string, reasoningEffort?: ReasoningEffort): Promise<void> {
await this.rpc.model.switchTo({ modelId: model, ...(reasoningEffort !== undefined && { reasoningEffort }) });
}
}
49 changes: 49 additions & 0 deletions nodejs/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,55 @@ describe("CopilotClient", () => {
spy.mockRestore();
});

it("sends session.model.switchTo RPC with reasoningEffort when provided", async () => {
const client = new CopilotClient();
await client.start();
onTestFinished(() => client.forceStop());

const session = await client.createSession({ onPermissionRequest: approveAll });

// Mock sendRequest to capture the call without hitting the runtime
const spy = vi
.spyOn((client as any).connection!, "sendRequest")
.mockImplementation(async (method: string, _params: any) => {
if (method === "session.model.switchTo") return {};
throw new Error(`Unexpected method: ${method}`);
});

await session.setModel("gpt-4.1", "high");

expect(spy).toHaveBeenCalledWith("session.model.switchTo", {
sessionId: session.sessionId,
modelId: "gpt-4.1",
reasoningEffort: "high",
});

spy.mockRestore();
});

it("sends session.model.switchTo RPC without reasoningEffort when not provided", async () => {
const client = new CopilotClient();
await client.start();
onTestFinished(() => client.forceStop());

const session = await client.createSession({ onPermissionRequest: approveAll });

// Mock sendRequest to capture the call without hitting the runtime
const spy = vi
.spyOn((client as any).connection!, "sendRequest")
.mockImplementation(async (method: string, params: any) => {
if (method === "session.model.switchTo") return {};
throw new Error(`Unexpected method: ${method}`);
});

await session.setModel("gpt-4.1");

const callArgs = spy.mock.calls[0][1] as Record<string, unknown>;
expect(callArgs).not.toHaveProperty("reasoningEffort");

spy.mockRestore();
});

describe("URL parsing", () => {
it("should parse port-only URL format", () => {
const client = new CopilotClient({
Expand Down
6 changes: 5 additions & 1 deletion python/copilot/generated/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,16 +468,20 @@ def to_dict(self) -> dict:
@dataclass
class SessionModelSwitchToParams:
model_id: str
reasoning_effort: str | None = None

@staticmethod
def from_dict(obj: Any) -> 'SessionModelSwitchToParams':
assert isinstance(obj, dict)
model_id = from_str(obj.get("modelId"))
return SessionModelSwitchToParams(model_id)
reasoning_effort = from_union([from_str, from_none], obj.get("reasoningEffort"))
return SessionModelSwitchToParams(model_id, reasoning_effort)

def to_dict(self) -> dict:
result: dict = {}
result["modelId"] = from_str(self.model_id)
if self.reasoning_effort is not None:
result["reasoningEffort"] = from_str(self.reasoning_effort)
return result


Expand Down
8 changes: 6 additions & 2 deletions python/copilot/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
MessageOptions,
PermissionRequest,
PermissionRequestResult,
ReasoningEffort,
SessionHooks,
Tool,
ToolHandler,
Expand Down Expand Up @@ -521,7 +522,7 @@ async def abort(self) -> None:
"""
await self._client.request("session.abort", {"sessionId": self.session_id})

async def set_model(self, model: str) -> None:
async def set_model(self, model: str, reasoning_effort: ReasoningEffort | None = None) -> None:
"""
Change the model for this session.

Expand All @@ -530,11 +531,14 @@ async def set_model(self, model: str) -> None:

Args:
model: Model ID to switch to (e.g., "gpt-4.1", "claude-sonnet-4").
reasoning_effort: Optional reasoning effort level for models that support it
(e.g., "low", "medium", "high", "xhigh").

Raises:
Exception: If the session has been destroyed or the connection fails.

Example:
>>> await session.set_model("gpt-4.1")
>>> await session.set_model("gpt-4.1", "high")
"""
await self.rpc.model.switch_to(SessionModelSwitchToParams(model_id=model))
await self.rpc.model.switch_to(SessionModelSwitchToParams(model_id=model, reasoning_effort=reasoning_effort))
28 changes: 28 additions & 0 deletions python/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,5 +314,33 @@ async def mock_request(method, params):
await session.set_model("gpt-4.1")
assert captured["session.model.switchTo"]["sessionId"] == session.session_id
assert captured["session.model.switchTo"]["modelId"] == "gpt-4.1"
assert "reasoningEffort" not in captured["session.model.switchTo"]
finally:
await client.force_stop()

@pytest.mark.asyncio
async def test_set_model_with_reasoning_effort_sends_correct_rpc(self):
client = CopilotClient({"cli_path": CLI_PATH})
await client.start()

try:
session = await client.create_session(
{"on_permission_request": PermissionHandler.approve_all}
)

captured = {}
original_request = client._client.request

async def mock_request(method, params):
captured[method] = params
if method == "session.model.switchTo":
return {}
return await original_request(method, params)

client._client.request = mock_request
await session.set_model("gpt-4.1", "high")
assert captured["session.model.switchTo"]["sessionId"] == session.session_id
assert captured["session.model.switchTo"]["modelId"] == "gpt-4.1"
assert captured["session.model.switchTo"]["reasoningEffort"] == "high"
finally:
await client.force_stop()
32 changes: 0 additions & 32 deletions test/snapshots/builtin_tools/should_find_files_by_pattern.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,35 +53,3 @@ conversations:
Found 2 TypeScript files:
- `src/app.ts`
- `src/index.ts`
# Windows returns files in alphabetical order
- messages:
- role: system
content: ${system}
- role: user
content: Find all .ts files in this directory (recursively). List the filenames you found.
- role: assistant
content: I'll search for all TypeScript files recursively in the current directory.
tool_calls:
- id: toolcall_0
type: function
function:
name: report_intent
arguments: '{"intent":"Finding TypeScript files"}'
- id: toolcall_1
type: function
function:
name: glob
arguments: '{"pattern":"**/*.ts"}'
- role: tool
tool_call_id: toolcall_0
content: Intent logged
- role: tool
tool_call_id: toolcall_1
content: |-
./src/app.ts
./src/index.ts
- role: assistant
content: |-
Found 2 TypeScript files:
- `src/app.ts`
- `src/index.ts`
Loading