Skip to content

Commit ea90f07

Browse files
authored
Add reasoningEffort to setModel/session.model.switchTo across all SDKs (#712)
1 parent a0bbde9 commit ea90f07

File tree

13 files changed

+175
-17
lines changed

13 files changed

+175
-17
lines changed

dotnet/src/Session.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -720,15 +720,25 @@ await InvokeRpcAsync<object>(
720720
/// The new model takes effect for the next message. Conversation history is preserved.
721721
/// </summary>
722722
/// <param name="model">Model ID to switch to (e.g., "gpt-4.1").</param>
723+
/// <param name="reasoningEffort">Reasoning effort level (e.g., "low", "medium", "high", "xhigh").</param>
723724
/// <param name="cancellationToken">Optional cancellation token.</param>
724725
/// <example>
725726
/// <code>
726727
/// await session.SetModelAsync("gpt-4.1");
728+
/// await session.SetModelAsync("claude-sonnet-4.6", "high");
727729
/// </code>
728730
/// </example>
729-
public async Task SetModelAsync(string model, CancellationToken cancellationToken = default)
731+
public async Task SetModelAsync(string model, string? reasoningEffort, CancellationToken cancellationToken = default)
730732
{
731-
await Rpc.Model.SwitchToAsync(model, cancellationToken: cancellationToken);
733+
await Rpc.Model.SwitchToAsync(model, reasoningEffort, cancellationToken);
734+
}
735+
736+
/// <summary>
737+
/// Changes the model for this session.
738+
/// </summary>
739+
public Task SetModelAsync(string model, CancellationToken cancellationToken = default)
740+
{
741+
return SetModelAsync(model, reasoningEffort: null, cancellationToken);
732742
}
733743

734744
/// <summary>

dotnet/test/RpcTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ public async Task Should_Call_Session_Rpc_Model_SwitchTo()
7272
var before = await session.Rpc.Model.GetCurrentAsync();
7373
Assert.NotNull(before.ModelId);
7474

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

7979
// Verify the switch persisted

dotnet/test/SessionTests.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,20 @@ public async Task Should_Set_Model_On_Existing_Session()
440440
Assert.Equal("gpt-4.1", modelChanged.Data.NewModel);
441441
}
442442

443+
[Fact]
444+
public async Task Should_Set_Model_With_ReasoningEffort()
445+
{
446+
var session = await CreateSessionAsync();
447+
448+
var modelChangedTask = TestHelper.GetNextEventOfTypeAsync<SessionModelChangeEvent>(session);
449+
450+
await session.SetModelAsync("gpt-4.1", "high");
451+
452+
var modelChanged = await modelChangedTask;
453+
Assert.Equal("gpt-4.1", modelChanged.Data.NewModel);
454+
Assert.Equal("high", modelChanged.Data.ReasoningEffort);
455+
}
456+
443457
[Fact]
444458
public async Task Should_Log_Messages_At_Various_Levels()
445459
{

go/internal/e2e/rpc_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,11 @@ func TestSessionRpc(t *testing.T) {
168168
t.Error("Expected initial modelId to be defined")
169169
}
170170

171-
// Switch to a different model
171+
// Switch to a different model with reasoning effort
172+
re := "high"
172173
result, err := session.RPC.Model.SwitchTo(t.Context(), &rpc.SessionModelSwitchToParams{
173-
ModelID: "gpt-4.1",
174+
ModelID: "gpt-4.1",
175+
ReasoningEffort: &re,
174176
})
175177
if err != nil {
176178
t.Fatalf("Failed to switch model: %v", err)
@@ -201,7 +203,7 @@ func TestSessionRpc(t *testing.T) {
201203
t.Fatalf("Failed to create session: %v", err)
202204
}
203205

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

go/internal/e2e/session_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,49 @@ func getSystemMessage(exchange testharness.ParsedHttpExchange) string {
895895
return ""
896896
}
897897

898+
func TestSetModelWithReasoningEffort(t *testing.T) {
899+
ctx := testharness.NewTestContext(t)
900+
client := ctx.NewClient()
901+
t.Cleanup(func() { client.ForceStop() })
902+
903+
if err := client.Start(t.Context()); err != nil {
904+
t.Fatalf("Failed to start client: %v", err)
905+
}
906+
907+
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
908+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
909+
})
910+
if err != nil {
911+
t.Fatalf("Failed to create session: %v", err)
912+
}
913+
914+
modelChanged := make(chan copilot.SessionEvent, 1)
915+
session.On(func(event copilot.SessionEvent) {
916+
if event.Type == copilot.SessionModelChange {
917+
select {
918+
case modelChanged <- event:
919+
default:
920+
}
921+
}
922+
})
923+
924+
if err := session.SetModel(t.Context(), "gpt-4.1", copilot.SetModelOptions{ReasoningEffort: "high"}); err != nil {
925+
t.Fatalf("SetModel returned error: %v", err)
926+
}
927+
928+
select {
929+
case evt := <-modelChanged:
930+
if evt.Data.NewModel == nil || *evt.Data.NewModel != "gpt-4.1" {
931+
t.Errorf("Expected newModel 'gpt-4.1', got %v", evt.Data.NewModel)
932+
}
933+
if evt.Data.ReasoningEffort == nil || *evt.Data.ReasoningEffort != "high" {
934+
t.Errorf("Expected reasoningEffort 'high', got %v", evt.Data.ReasoningEffort)
935+
}
936+
case <-time.After(30 * time.Second):
937+
t.Fatal("Timed out waiting for session.model_change event")
938+
}
939+
}
940+
898941
func getToolNames(exchange testharness.ParsedHttpExchange) []string {
899942
var names []string
900943
for _, tool := range exchange.Request.Tools {

go/session.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,12 @@ func (s *Session) Abort(ctx context.Context) error {
737737
return nil
738738
}
739739

740+
// SetModelOptions configures optional parameters for SetModel.
741+
type SetModelOptions struct {
742+
// ReasoningEffort sets the reasoning effort level for the new model (e.g., "low", "medium", "high", "xhigh").
743+
ReasoningEffort string
744+
}
745+
740746
// SetModel changes the model for this session.
741747
// The new model takes effect for the next message. Conversation history is preserved.
742748
//
@@ -745,8 +751,16 @@ func (s *Session) Abort(ctx context.Context) error {
745751
// if err := session.SetModel(context.Background(), "gpt-4.1"); err != nil {
746752
// log.Printf("Failed to set model: %v", err)
747753
// }
748-
func (s *Session) SetModel(ctx context.Context, model string) error {
749-
_, err := s.RPC.Model.SwitchTo(ctx, &rpc.SessionModelSwitchToParams{ModelID: model})
754+
// if err := session.SetModel(context.Background(), "claude-sonnet-4.6", SetModelOptions{ReasoningEffort: "high"}); err != nil {
755+
// log.Printf("Failed to set model: %v", err)
756+
// }
757+
func (s *Session) SetModel(ctx context.Context, model string, opts ...SetModelOptions) error {
758+
params := &rpc.SessionModelSwitchToParams{ModelID: model}
759+
if len(opts) > 0 && opts[0].ReasoningEffort != "" {
760+
re := opts[0].ReasoningEffort
761+
params.ReasoningEffort = &re
762+
}
763+
_, err := s.RPC.Model.SwitchTo(ctx, params)
750764
if err != nil {
751765
return fmt.Errorf("failed to set model: %w", err)
752766
}

nodejs/src/session.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
PermissionHandler,
1717
PermissionRequest,
1818
PermissionRequestResult,
19+
ReasoningEffort,
1920
SessionEvent,
2021
SessionEventHandler,
2122
SessionEventPayload,
@@ -718,14 +719,16 @@ export class CopilotSession {
718719
* The new model takes effect for the next message. Conversation history is preserved.
719720
*
720721
* @param model - Model ID to switch to
722+
* @param options - Optional settings for the new model
721723
*
722724
* @example
723725
* ```typescript
724726
* await session.setModel("gpt-4.1");
727+
* await session.setModel("claude-sonnet-4.6", { reasoningEffort: "high" });
725728
* ```
726729
*/
727-
async setModel(model: string): Promise<void> {
728-
await this.rpc.model.switchTo({ modelId: model });
730+
async setModel(model: string, options?: { reasoningEffort?: ReasoningEffort }): Promise<void> {
731+
await this.rpc.model.switchTo({ modelId: model, ...options });
729732
}
730733

731734
/**

nodejs/test/client.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,31 @@ describe("CopilotClient", () => {
123123
spy.mockRestore();
124124
});
125125

126+
it("sends reasoningEffort with session.model.switchTo when provided", async () => {
127+
const client = new CopilotClient();
128+
await client.start();
129+
onTestFinished(() => client.forceStop());
130+
131+
const session = await client.createSession({ onPermissionRequest: approveAll });
132+
133+
const spy = vi
134+
.spyOn((client as any).connection!, "sendRequest")
135+
.mockImplementation(async (method: string, _params: any) => {
136+
if (method === "session.model.switchTo") return {};
137+
throw new Error(`Unexpected method: ${method}`);
138+
});
139+
140+
await session.setModel("claude-sonnet-4.6", { reasoningEffort: "high" });
141+
142+
expect(spy).toHaveBeenCalledWith("session.model.switchTo", {
143+
sessionId: session.sessionId,
144+
modelId: "claude-sonnet-4.6",
145+
reasoningEffort: "high",
146+
});
147+
148+
spy.mockRestore();
149+
});
150+
126151
describe("URL parsing", () => {
127152
it("should parse port-only URL format", () => {
128153
const client = new CopilotClient({

nodejs/test/e2e/rpc.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,11 @@ describe("Session RPC", async () => {
9292
const before = await session.rpc.model.getCurrent();
9393
expect(before.modelId).toBeDefined();
9494

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

99102
// Verify the switch persisted

nodejs/test/e2e/session.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,4 +461,16 @@ describe("Send Blocking Behavior", async () => {
461461
session.sendAndWait({ prompt: "Run 'sleep 2 && echo done'" }, 100)
462462
).rejects.toThrow(/Timeout after 100ms/);
463463
});
464+
465+
it("should set model with reasoningEffort", async () => {
466+
const session = await client.createSession({ onPermissionRequest: approveAll });
467+
468+
const modelChangePromise = getNextEventOfType(session, "session.model_change");
469+
470+
await session.setModel("gpt-4.1", { reasoningEffort: "high" });
471+
472+
const event = await modelChangePromise;
473+
expect(event.data.newModel).toBe("gpt-4.1");
474+
expect(event.data.reasoningEffort).toBe("high");
475+
});
464476
});

0 commit comments

Comments
 (0)