From 518256ddc811e01d60d4a137e9cb08c0f9fb4d50 Mon Sep 17 00:00:00 2001 From: Patrick Nikoletich Date: Sat, 7 Mar 2026 20:54:55 -0800 Subject: [PATCH] Add reasoningEffort to setModel/session.model.switchTo across all SDKs All four SDKs now support passing reasoningEffort when switching models mid-session via setModel(). The parameter is optional and backward-compatible. - Node.js: setModel(model, { reasoningEffort? }) - Python: set_model(model, *, reasoning_effort=None) - Go: SetModel(ctx, model, opts ...*SetModelOptions) - .NET: SetModelAsync(model, reasoningEffort?, cancellationToken?) Fixes #687 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Generated/Rpc.cs | 21 +++++++- dotnet/src/Session.cs | 14 ++++- dotnet/test/RpcTests.cs | 2 +- go/internal/e2e/rpc_test.go | 2 +- go/rpc/generated_rpc.go | 62 +++++++++++++++++++++- go/session.go | 21 ++++++-- nodejs/src/generated/rpc.ts | 29 +++++++++++ nodejs/src/session.ts | 9 +++- nodejs/test/client.test.ts | 25 +++++++++ python/copilot/generated/rpc.py | 92 ++++++++++++++++++++++++++++++++- python/copilot/session.py | 12 ++++- 11 files changed, 274 insertions(+), 15 deletions(-) diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 85e55e4b8..3bc4b6768 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 SessionModelSwitchToRequestReasoningEffort? ReasoningEffort { get; set; } } public class SessionModeGetResult @@ -511,6 +514,20 @@ internal class SessionPermissionsHandlePendingPermissionRequestRequest public object Result { get; set; } = null!; } +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum SessionModelSwitchToRequestReasoningEffort +{ + [JsonStringEnumMemberName("low")] + Low, + [JsonStringEnumMemberName("medium")] + Medium, + [JsonStringEnumMemberName("high")] + High, + [JsonStringEnumMemberName("xhigh")] + Xhigh, +} + + [JsonConverter(typeof(JsonStringEnumConverter))] public enum SessionModeGetResultMode { @@ -664,9 +681,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, SessionModelSwitchToRequestReasoningEffort? reasoningEffort, 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 397eae0fa..4699c71ff 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -674,15 +674,25 @@ 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 (e.g., "low", "medium", "high", "xhigh"). /// Optional cancellation token. /// /// /// await session.SetModelAsync("gpt-4.1"); + /// await session.SetModelAsync("claude-sonnet-4.6", SessionModelSwitchToRequestReasoningEffort.High); /// /// - public async Task SetModelAsync(string model, CancellationToken cancellationToken = default) + public async Task SetModelAsync(string model, SessionModelSwitchToRequestReasoningEffort? reasoningEffort = null, CancellationToken cancellationToken = default) { - await Rpc.Model.SwitchToAsync(model, cancellationToken); + await Rpc.Model.SwitchToAsync(model, reasoningEffort, cancellationToken); + } + + /// + /// Changes the model for this session (backward-compatible overload). + /// + public Task SetModelAsync(string model, CancellationToken cancellationToken) + { + return SetModelAsync(model, reasoningEffort: null, cancellationToken); } /// diff --git a/dotnet/test/RpcTests.cs b/dotnet/test/RpcTests.cs index a13695589..7e3c862b7 100644 --- a/dotnet/test/RpcTests.cs +++ b/dotnet/test/RpcTests.cs @@ -73,7 +73,7 @@ public async Task Should_Call_Session_Rpc_Model_SwitchTo() Assert.NotNull(before.ModelId); // Switch to a different model - var result = await session.Rpc.Model.SwitchToAsync(modelId: "gpt-4.1"); + var result = await session.Rpc.Model.SwitchToAsync(modelId: "gpt-4.1", reasoningEffort: null); Assert.Equal("gpt-4.1", result.ModelId); // Verify the switch persisted diff --git a/go/internal/e2e/rpc_test.go b/go/internal/e2e/rpc_test.go index 61a5e338d..ac4a5b0d7 100644 --- a/go/internal/e2e/rpc_test.go +++ b/go/internal/e2e/rpc_test.go @@ -201,7 +201,7 @@ 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) } }) diff --git a/go/rpc/generated_rpc.go b/go/rpc/generated_rpc.go index 67a354202..0e4b96e4f 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 *ReasoningEffort `json:"reasoningEffort,omitempty"` } type SessionModeGetResult struct { @@ -296,6 +297,30 @@ type SessionPermissionsHandlePendingPermissionRequestParamsResult struct { Path *string `json:"path,omitempty"` } +type SessionLogResult struct { + // The unique identifier of the emitted session event + EventID string `json:"eventId"` +} + +type SessionLogParams struct { + // When true, the message is transient and not persisted to the session event log on disk + Ephemeral *bool `json:"ephemeral,omitempty"` + // Log severity level. Determines how the message is displayed in the timeline. Defaults to + // "info". + Level *Level `json:"level,omitempty"` + // Human-readable message + Message string `json:"message"` +} + +type ReasoningEffort string + +const ( + High ReasoningEffort = "high" + Low ReasoningEffort = "low" + Medium ReasoningEffort = "medium" + Xhigh ReasoningEffort = "xhigh" +) + // The current agent mode. // // The agent mode after switching. @@ -319,6 +344,16 @@ const ( DeniedNoApprovalRuleAndCouldNotRequestFromUser Kind = "denied-no-approval-rule-and-could-not-request-from-user" ) +// Log severity level. Determines how the message is displayed in the timeline. Defaults to +// "info". +type Level string + +const ( + Error Level = "error" + Info Level = "info" + Warning Level = "warning" +) + type ResultUnion struct { ResultResult *ResultResult String *string @@ -416,6 +451,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 { @@ -725,6 +763,28 @@ type SessionRpc struct { Permissions *PermissionsRpcApi } +func (a *SessionRpc) Log(ctx context.Context, params *SessionLogParams) (*SessionLogResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + if params != nil { + req["message"] = params.Message + if params.Level != nil { + req["level"] = *params.Level + } + if params.Ephemeral != nil { + req["ephemeral"] = *params.Ephemeral + } + } + raw, err := a.client.Request("session.log", req) + if err != nil { + return nil, err + } + var result SessionLogResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + func NewSessionRpc(client *jsonrpc2.Client, sessionID string) *SessionRpc { return &SessionRpc{client: client, sessionID: sessionID, Model: &ModelRpcApi{client: client, sessionID: sessionID}, diff --git a/go/session.go b/go/session.go index c06a8e1ec..1ae42afd6 100644 --- a/go/session.go +++ b/go/session.go @@ -685,16 +685,31 @@ func (s *Session) Abort(ctx context.Context) error { return nil } +// SetModelOptions configures optional parameters for SetModel. +type SetModelOptions struct { + // ReasoningEffort sets the reasoning effort level for the new model (e.g., "low", "medium", "high", "xhigh"). + ReasoningEffort rpc.ReasoningEffort +} + // SetModel changes the model for this session. // The new model takes effect for the next message. Conversation history is preserved. +// Pass nil for opts if no additional options are needed. // // 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}) +// if err := session.SetModel(context.Background(), "claude-sonnet-4.6", &SetModelOptions{ReasoningEffort: "high"}); err != nil { +// log.Printf("Failed to set model: %v", err) +// } +func (s *Session) SetModel(ctx context.Context, model string, opts *SetModelOptions) error { + params := &rpc.SessionModelSwitchToParams{ModelID: model} + if opts != nil && opts.ReasoningEffort != "" { + re := opts.ReasoningEffort + params.ReasoningEffort = &re + } + _, err := s.RPC.Model.SwitchTo(ctx, params) 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 c230348e0..ec40bfa69 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?: "low" | "medium" | "high" | "xhigh"; } export interface SessionModeGetResult { @@ -489,6 +490,32 @@ export interface SessionPermissionsHandlePendingPermissionRequestParams { }; } +export interface SessionLogResult { + /** + * The unique identifier of the emitted session event + */ + eventId: string; +} + +export interface SessionLogParams { + /** + * Target session identifier + */ + sessionId: string; + /** + * Human-readable message + */ + message: string; + /** + * Log severity level. Determines how the message is displayed in the timeline. Defaults to "info". + */ + level?: "info" | "warning" | "error"; + /** + * When true, the message is transient and not persisted to the session event log on disk + */ + ephemeral?: boolean; +} + /** Create typed server-scoped RPC methods (no session required). */ export function createServerRpc(connection: MessageConnection) { return { @@ -566,5 +593,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin handlePendingPermissionRequest: async (params: Omit): Promise => connection.sendRequest("session.permissions.handlePendingPermissionRequest", { sessionId, ...params }), }, + log: async (params: Omit): Promise => + connection.sendRequest("session.log", { sessionId, ...params }), }; } diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 181d1a961..4088ec706 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -684,13 +684,18 @@ export class CopilotSession { * The new model takes effect for the next message. Conversation history is preserved. * * @param model - Model ID to switch to + * @param options - Optional settings for the new model * * @example * ```typescript * await session.setModel("gpt-4.1"); + * await session.setModel("claude-sonnet-4.6", { reasoningEffort: "high" }); * ``` */ - async setModel(model: string): Promise { - await this.rpc.model.switchTo({ modelId: model }); + async setModel( + model: string, + options?: { reasoningEffort?: "low" | "medium" | "high" | "xhigh" } + ): Promise { + await this.rpc.model.switchTo({ modelId: model, ...options }); } } diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index b7dd34395..61abee172 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -91,6 +91,31 @@ describe("CopilotClient", () => { spy.mockRestore(); }); + it("sends reasoningEffort with session.model.switchTo when provided", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + + 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("claude-sonnet-4.6", { reasoningEffort: "high" }); + + expect(spy).toHaveBeenCalledWith("session.model.switchTo", { + sessionId: session.sessionId, + modelId: "claude-sonnet-4.6", + reasoningEffort: "high", + }); + + 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 ef188b095..d5fa7b73b 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -13,6 +13,7 @@ from typing import Any, TypeVar, cast from collections.abc import Callable from enum import Enum +from uuid import UUID T = TypeVar("T") @@ -465,19 +466,30 @@ def to_dict(self) -> dict: return result +class ReasoningEffort(Enum): + HIGH = "high" + LOW = "low" + MEDIUM = "medium" + XHIGH = "xhigh" + + @dataclass class SessionModelSwitchToParams: model_id: str + reasoning_effort: ReasoningEffort | 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([ReasoningEffort, 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_union([lambda x: to_enum(ReasoningEffort, x), from_none], self.reasoning_effort) return result @@ -1065,6 +1077,63 @@ def to_dict(self) -> dict: return result +@dataclass +class SessionLogResult: + event_id: UUID + """The unique identifier of the emitted session event""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionLogResult': + assert isinstance(obj, dict) + event_id = UUID(obj.get("eventId")) + return SessionLogResult(event_id) + + def to_dict(self) -> dict: + result: dict = {} + result["eventId"] = str(self.event_id) + return result + + +class Level(Enum): + """Log severity level. Determines how the message is displayed in the timeline. Defaults to + "info". + """ + ERROR = "error" + INFO = "info" + WARNING = "warning" + + +@dataclass +class SessionLogParams: + message: str + """Human-readable message""" + + ephemeral: bool | None = None + """When true, the message is transient and not persisted to the session event log on disk""" + + level: Level | None = None + """Log severity level. Determines how the message is displayed in the timeline. Defaults to + "info". + """ + + @staticmethod + def from_dict(obj: Any) -> 'SessionLogParams': + assert isinstance(obj, dict) + message = from_str(obj.get("message")) + ephemeral = from_union([from_bool, from_none], obj.get("ephemeral")) + level = from_union([Level, from_none], obj.get("level")) + return SessionLogParams(message, ephemeral, level) + + def to_dict(self) -> dict: + result: dict = {} + result["message"] = from_str(self.message) + if self.ephemeral is not None: + result["ephemeral"] = from_union([from_bool, from_none], self.ephemeral) + if self.level is not None: + result["level"] = from_union([lambda x: to_enum(Level, x), from_none], self.level) + return result + + def ping_result_from_dict(s: Any) -> PingResult: return PingResult.from_dict(s) @@ -1329,6 +1398,22 @@ def session_permissions_handle_pending_permission_request_params_to_dict(x: Sess return to_class(SessionPermissionsHandlePendingPermissionRequestParams, x) +def session_log_result_from_dict(s: Any) -> SessionLogResult: + return SessionLogResult.from_dict(s) + + +def session_log_result_to_dict(x: SessionLogResult) -> Any: + return to_class(SessionLogResult, x) + + +def session_log_params_from_dict(s: Any) -> SessionLogParams: + return SessionLogParams.from_dict(s) + + +def session_log_params_to_dict(x: SessionLogParams) -> Any: + return to_class(SessionLogParams, x) + + def _timeout_kwargs(timeout: float | None) -> dict: """Build keyword arguments for optional timeout forwarding.""" if timeout is not None: @@ -1515,3 +1600,8 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self.tools = ToolsApi(client, session_id) self.permissions = PermissionsApi(client, session_id) + async def log(self, params: SessionLogParams, *, timeout: float | None = None) -> SessionLogResult: + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return SessionLogResult.from_dict(await self._client.request("session.log", params_dict, **_timeout_kwargs(timeout))) + diff --git a/python/copilot/session.py b/python/copilot/session.py index e0e72fc68..e73c497a6 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -716,7 +716,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: str | None = None) -> None: """ Change the model for this session. @@ -725,11 +725,19 @@ 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 the new model + (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("claude-sonnet-4.6", reasoning_effort="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, + ) + )