diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 4c4bac0f3..319473178 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -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 @@ -615,9 +618,9 @@ public async Task GetCurrentAsync(CancellationToke } /// Calls "session.model.switchTo". - public async Task SwitchToAsync(string modelId, CancellationToken cancellationToken = default) + public async Task 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(_rpc, "session.model.switchTo", [request], cancellationToken); } } diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index f348f70d8..e70bc2768 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -510,15 +510,17 @@ await InvokeRpcAsync( /// The new model takes effect for the next message. Conversation history is preserved. /// /// Model ID to switch to (e.g., "gpt-4.1"). + /// Optional reasoning effort level for models that support it (e.g., "low", "medium", "high", "xhigh"). /// Optional cancellation token. /// /// /// await session.SetModelAsync("gpt-4.1"); + /// await session.SetModelAsync("gpt-4.1", reasoningEffort: "high"); /// /// - 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); } /// diff --git a/dotnet/test/SessionTests.cs b/dotnet/test/SessionTests.cs index ebeb75612..e6a477022 100644 --- a/dotnet/test/SessionTests.cs +++ b/dotnet/test/SessionTests.cs @@ -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(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); + } } diff --git a/go/internal/e2e/rpc_test.go b/go/internal/e2e/rpc_test.go index 61a5e338d..42995723e 100644 --- a/go/internal/e2e/rpc_test.go +++ b/go/internal/e2e/rpc_test.go @@ -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 { diff --git a/go/rpc/generated_rpc.go b/go/rpc/generated_rpc.go index 858a8032d..bb230aed4 100644 --- a/go/rpc/generated_rpc.go +++ b/go/rpc/generated_rpc.go @@ -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 { @@ -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 { diff --git a/go/session.go b/go/session.go index 2d7146eb8..f5af4209b 100644 --- a/go/session.go +++ b/go/session.go @@ -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) } diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index af6d27783..ea4748d44 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -173,6 +173,7 @@ export interface SessionModelSwitchToParams { */ sessionId: string; modelId: string; + reasoningEffort?: string; } export interface SessionModeGetResult { diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index f7b0ee585..3fba25342 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -14,6 +14,7 @@ import type { PermissionHandler, PermissionRequest, PermissionRequestResult, + ReasoningEffort, SessionEvent, SessionEventHandler, SessionEventPayload, @@ -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 { - await this.rpc.model.switchTo({ modelId: model }); + async setModel(model: string, reasoningEffort?: ReasoningEffort): Promise { + await this.rpc.model.switchTo({ modelId: model, ...(reasoningEffort !== undefined && { reasoningEffort }) }); } } diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 2fa8eb434..06f4b0f23 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -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; + expect(callArgs).not.toHaveProperty("reasoningEffort"); + + spy.mockRestore(); + }); + describe("URL parsing", () => { it("should parse port-only URL format", () => { const client = new CopilotClient({ diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index ed199f138..b546595b5 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -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 diff --git a/python/copilot/session.py b/python/copilot/session.py index 1fec27ef7..cc2942b15 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -17,6 +17,7 @@ MessageOptions, PermissionRequest, PermissionRequestResult, + ReasoningEffort, SessionHooks, Tool, ToolHandler, @@ -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. @@ -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)) diff --git a/python/test_client.py b/python/test_client.py index 05b324228..7260d93e4 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -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() diff --git a/test/snapshots/builtin_tools/should_find_files_by_pattern.yaml b/test/snapshots/builtin_tools/should_find_files_by_pattern.yaml index 7dfc9ec98..c1e74e505 100644 --- a/test/snapshots/builtin_tools/should_find_files_by_pattern.yaml +++ b/test/snapshots/builtin_tools/should_find_files_by_pattern.yaml @@ -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`