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
14 changes: 12 additions & 2 deletions dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -720,15 +720,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">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", "high");
/// </code>
/// </example>
public async Task SetModelAsync(string model, CancellationToken cancellationToken = default)
public async Task SetModelAsync(string model, string? reasoningEffort, CancellationToken cancellationToken = default)
{
await Rpc.Model.SwitchToAsync(model, cancellationToken: cancellationToken);
await Rpc.Model.SwitchToAsync(model, reasoningEffort, cancellationToken);
}

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

/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions dotnet/test/RpcTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ public async Task Should_Call_Session_Rpc_Model_SwitchTo()
var before = await session.Rpc.Model.GetCurrentAsync();
Assert.NotNull(before.ModelId);

// Switch to a different model
var result = await session.Rpc.Model.SwitchToAsync(modelId: "gpt-4.1");
// Switch to a different model with reasoning effort
var result = await session.Rpc.Model.SwitchToAsync(modelId: "gpt-4.1", reasoningEffort: "high");
Assert.Equal("gpt-4.1", result.ModelId);

// Verify the switch persisted
Expand Down
14 changes: 14 additions & 0 deletions dotnet/test/SessionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,20 @@ public async Task Should_Set_Model_On_Existing_Session()
Assert.Equal("gpt-4.1", modelChanged.Data.NewModel);
}

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

var modelChangedTask = TestHelper.GetNextEventOfTypeAsync<SessionModelChangeEvent>(session);

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

var modelChanged = await modelChangedTask;
Assert.Equal("gpt-4.1", modelChanged.Data.NewModel);
Assert.Equal("high", modelChanged.Data.ReasoningEffort);
}

[Fact]
public async Task Should_Log_Messages_At_Various_Levels()
{
Expand Down
8 changes: 5 additions & 3 deletions go/internal/e2e/rpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,11 @@ func TestSessionRpc(t *testing.T) {
t.Error("Expected initial modelId to be defined")
}

// Switch to a different model
// Switch to a different model with reasoning effort
re := "high"
result, err := session.RPC.Model.SwitchTo(t.Context(), &rpc.SessionModelSwitchToParams{
ModelID: "gpt-4.1",
ModelID: "gpt-4.1",
ReasoningEffort: &re,
})
if err != nil {
t.Fatalf("Failed to switch model: %v", err)
Expand Down Expand Up @@ -201,7 +203,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", copilot.SetModelOptions{ReasoningEffort: "high"}); err != nil {
t.Fatalf("SetModel returned error: %v", err)
}
})
Expand Down
43 changes: 43 additions & 0 deletions go/internal/e2e/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,49 @@ func getSystemMessage(exchange testharness.ParsedHttpExchange) string {
return ""
}

func TestSetModelWithReasoningEffort(t *testing.T) {
ctx := testharness.NewTestContext(t)
client := ctx.NewClient()
t.Cleanup(func() { client.ForceStop() })

if err := client.Start(t.Context()); err != nil {
t.Fatalf("Failed to start client: %v", err)
}

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

modelChanged := make(chan copilot.SessionEvent, 1)
session.On(func(event copilot.SessionEvent) {
if event.Type == copilot.SessionModelChange {
select {
case modelChanged <- event:
default:
}
}
})

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

select {
case evt := <-modelChanged:
if evt.Data.NewModel == nil || *evt.Data.NewModel != "gpt-4.1" {
t.Errorf("Expected newModel 'gpt-4.1', got %v", evt.Data.NewModel)
}
if evt.Data.ReasoningEffort == nil || *evt.Data.ReasoningEffort != "high" {
t.Errorf("Expected reasoningEffort 'high', got %v", evt.Data.ReasoningEffort)
}
case <-time.After(30 * time.Second):
t.Fatal("Timed out waiting for session.model_change event")
}
}

func getToolNames(exchange testharness.ParsedHttpExchange) []string {
var names []string
for _, tool := range exchange.Request.Tools {
Expand Down
18 changes: 16 additions & 2 deletions go/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,12 @@ 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 string
}

// SetModel changes the model for this session.
// The new model takes effect for the next message. Conversation history is preserved.
//
Expand All @@ -745,8 +751,16 @@ func (s *Session) Abort(ctx context.Context) error {
// if err := session.SetModel(context.Background(), "gpt-4.1"); 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 len(opts) > 0 && opts[0].ReasoningEffort != "" {
re := opts[0].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
7 changes: 5 additions & 2 deletions nodejs/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
PermissionHandler,
PermissionRequest,
PermissionRequestResult,
ReasoningEffort,
SessionEvent,
SessionEventHandler,
SessionEventPayload,
Expand Down Expand Up @@ -718,14 +719,16 @@ 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?: ReasoningEffort }): Promise<void> {
await this.rpc.model.switchTo({ modelId: model, ...options });
}

/**
Expand Down
25 changes: 25 additions & 0 deletions nodejs/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,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
7 changes: 5 additions & 2 deletions nodejs/test/e2e/rpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,11 @@ describe("Session RPC", async () => {
const before = await session.rpc.model.getCurrent();
expect(before.modelId).toBeDefined();

// Switch to a different model
const result = await session.rpc.model.switchTo({ modelId: "gpt-4.1" });
// Switch to a different model with reasoning effort
const result = await session.rpc.model.switchTo({
modelId: "gpt-4.1",
reasoningEffort: "high",
});
expect(result.modelId).toBe("gpt-4.1");

// Verify the switch persisted
Expand Down
12 changes: 12 additions & 0 deletions nodejs/test/e2e/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,4 +461,16 @@ describe("Send Blocking Behavior", async () => {
session.sendAndWait({ prompt: "Run 'sleep 2 && echo done'" }, 100)
).rejects.toThrow(/Timeout after 100ms/);
});

it("should set model with reasoningEffort", async () => {
const session = await client.createSession({ onPermissionRequest: approveAll });

const modelChangePromise = getNextEventOfType(session, "session.model_change");

await session.setModel("gpt-4.1", { reasoningEffort: "high" });

const event = await modelChangePromise;
expect(event.data.newModel).toBe("gpt-4.1");
expect(event.data.reasoningEffort).toBe("high");
});
});
12 changes: 10 additions & 2 deletions python/copilot/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,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.
Expand All @@ -737,14 +737,22 @@ 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,
)
)

async def log(
self,
Expand Down
6 changes: 4 additions & 2 deletions python/e2e/test_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,10 @@ async def test_should_call_session_rpc_model_switch_to(self, ctx: E2ETestContext
before = await session.rpc.model.get_current()
assert before.model_id is not None

# Switch to a different model
result = await session.rpc.model.switch_to(SessionModelSwitchToParams(model_id="gpt-4.1"))
# Switch to a different model with reasoning effort
result = await session.rpc.model.switch_to(
SessionModelSwitchToParams(model_id="gpt-4.1", reasoning_effort="high")
)
assert result.model_id == "gpt-4.1"

# Verify the switch persisted
Expand Down
22 changes: 22 additions & 0 deletions python/e2e/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,28 @@ def on_event(event):
assert by_message["Ephemeral message"].type.value == "session.info"
assert by_message["Ephemeral message"].data.info_type == "notification"

async def test_should_set_model_with_reasoning_effort(self, ctx: E2ETestContext):
"""Test that setModel passes reasoningEffort and it appears in the model_change event."""
import asyncio

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

model_change_event = asyncio.get_event_loop().create_future()

def on_event(event):
if not model_change_event.done() and event.type.value == "session.model_change":
model_change_event.set_result(event)

session.on(on_event)

await session.set_model("gpt-4.1", reasoning_effort="high")

event = await asyncio.wait_for(model_change_event, timeout=30)
assert event.data.new_model == "gpt-4.1"
assert event.data.reasoning_effort == "high"


def _get_system_message(exchange: dict) -> str:
messages = exchange.get("request", {}).get("messages", [])
Expand Down
Loading