Skip to content
Open
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
21 changes: 19 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 SessionModelSwitchToRequestReasoningEffort? ReasoningEffort { get; set; }
}

public class SessionModeGetResult
Expand Down Expand Up @@ -511,6 +514,20 @@ internal class SessionPermissionsHandlePendingPermissionRequestRequest
public object Result { get; set; } = null!;
}

[JsonConverter(typeof(JsonStringEnumConverter<SessionModelSwitchToRequestReasoningEffort>))]
public enum SessionModelSwitchToRequestReasoningEffort
{
[JsonStringEnumMemberName("low")]
Low,
[JsonStringEnumMemberName("medium")]
Medium,
[JsonStringEnumMemberName("high")]
High,
[JsonStringEnumMemberName("xhigh")]
Xhigh,
}


[JsonConverter(typeof(JsonStringEnumConverter<SessionModeGetResultMode>))]
public enum SessionModeGetResultMode
{
Expand Down Expand Up @@ -664,9 +681,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, SessionModelSwitchToRequestReasoningEffort? reasoningEffort, CancellationToken cancellationToken = default)
Copy link

@IeuanWalker IeuanWalker Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note, the reasoning effort is currently a string? everywhere else (getting list of models, creating/ resuming session, etc), so feels a bit weird that its an enum here.

As it would require us to parse the string? that the SDK returns into this enum to change it mid session

Wouldnt be an issue if it was an enum throughout SDK

{
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
14 changes: 12 additions & 2 deletions dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -674,15 +674,25 @@ 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 (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("claude-sonnet-4.6", SessionModelSwitchToRequestReasoningEffort.High);
/// </code>
/// </example>
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);
}

/// <summary>
/// Changes the model for this session (backward-compatible overload).
/// </summary>
public Task SetModelAsync(string model, CancellationToken cancellationToken)
{
return SetModelAsync(model, reasoningEffort: null, cancellationToken);
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion dotnet/test/RpcTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion go/internal/e2e/rpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
Expand Down
62 changes: 61 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 *ReasoningEffort `json:"reasoningEffort,omitempty"`
}

type SessionModeGetResult struct {
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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},
Expand Down
21 changes: 18 additions & 3 deletions go/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
29 changes: 29 additions & 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?: "low" | "medium" | "high" | "xhigh";
}

export interface SessionModeGetResult {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -566,5 +593,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin
handlePendingPermissionRequest: async (params: Omit<SessionPermissionsHandlePendingPermissionRequestParams, "sessionId">): Promise<SessionPermissionsHandlePendingPermissionRequestResult> =>
connection.sendRequest("session.permissions.handlePendingPermissionRequest", { sessionId, ...params }),
},
log: async (params: Omit<SessionLogParams, "sessionId">): Promise<SessionLogResult> =>
connection.sendRequest("session.log", { sessionId, ...params }),
};
}
9 changes: 7 additions & 2 deletions nodejs/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
await this.rpc.model.switchTo({ modelId: model });
async setModel(
model: string,
options?: { reasoningEffort?: "low" | "medium" | "high" | "xhigh" }
): Promise<void> {
await this.rpc.model.switchTo({ modelId: model, ...options });
}
}
25 changes: 25 additions & 0 deletions nodejs/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading
Loading