From a2c4917813a3ff4bdd3e13a92f7864b270456cf2 Mon Sep 17 00:00:00 2001 From: Tag Date: Wed, 25 Mar 2026 01:43:01 -0400 Subject: [PATCH] feat(agents): support dynamic model switching in Copilot SDK sessions --- src/agents/copilot-runner.ts | 5 +- .../copilot-sdk.dynamic-model-switch.test.ts | 91 +++++++++++++++++++ src/agents/copilot-sdk.ts | 20 +++- 3 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 src/agents/copilot-sdk.dynamic-model-switch.test.ts diff --git a/src/agents/copilot-runner.ts b/src/agents/copilot-runner.ts index 252708ccd43c0..baf9826fa887f 100644 --- a/src/agents/copilot-runner.ts +++ b/src/agents/copilot-runner.ts @@ -117,6 +117,9 @@ export async function runCopilotCliAgent(params: { const result = await runCopilotAgent({ prompt: params.prompt, model: modelId === "default" ? undefined : modelId, + // When resuming an existing session, use modelOverride to dynamically + // switch the model via setModel() instead of requiring a new session. + modelOverride: params.cliSessionId && modelId !== "default" ? modelId : undefined, workspaceDir: resolvedWorkspace, systemPrompt, timeoutMs: params.timeoutMs, @@ -133,7 +136,7 @@ export async function runCopilotCliAgent(params: { agentMeta: { sessionId: result.sessionId ?? params.sessionId ?? "", provider: "copilot-cli", - model: modelId, + model: result.model ?? modelId, }, }, }; diff --git a/src/agents/copilot-sdk.dynamic-model-switch.test.ts b/src/agents/copilot-sdk.dynamic-model-switch.test.ts new file mode 100644 index 0000000000000..a8070574c1657 --- /dev/null +++ b/src/agents/copilot-sdk.dynamic-model-switch.test.ts @@ -0,0 +1,91 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +// Mock the @github/copilot-sdk module +const mockSetModel = vi.fn().mockResolvedValue(undefined); +const mockSendAndWait = vi.fn().mockResolvedValue({ data: { content: "hello" } }); +const mockDestroy = vi.fn().mockResolvedValue(undefined); +const mockStop = vi.fn().mockResolvedValue(undefined); +const mockGetAuthStatus = vi.fn().mockResolvedValue({ + isAuthenticated: true, + authType: "token", + login: "test-user", +}); +const mockCreateSession = vi.fn().mockResolvedValue({ + sessionId: "sess-new", + setModel: mockSetModel, + sendAndWait: mockSendAndWait, + destroy: mockDestroy, +}); +const mockResumeSession = vi.fn().mockResolvedValue({ + sessionId: "sess-resumed", + setModel: mockSetModel, + sendAndWait: mockSendAndWait, + destroy: mockDestroy, +}); + +vi.mock("@github/copilot-sdk", () => { + const MockClient = vi.fn(function (this: Record) { + this.getAuthStatus = mockGetAuthStatus; + this.createSession = mockCreateSession; + this.resumeSession = mockResumeSession; + this.listModels = vi.fn().mockResolvedValue([]); + this.stop = mockStop; + }); + return { CopilotClient: MockClient }; +}); + +// Must import after mock setup +const { runCopilotAgent } = await import("./copilot-sdk.js"); + +beforeEach(() => { + vi.clearAllMocks(); + mockSendAndWait.mockResolvedValue({ data: { content: "response" } }); +}); + +describe("runCopilotAgent dynamic model switching", () => { + it("does not call setModel when modelOverride is not set", async () => { + const result = await runCopilotAgent({ + prompt: "hi", + model: "gpt-4o", + }); + + expect(mockSetModel).not.toHaveBeenCalled(); + expect(result.model).toBe("gpt-4o"); + expect(result.text).toBe("response"); + }); + + it("calls setModel when modelOverride is provided on new session", async () => { + const result = await runCopilotAgent({ + prompt: "hi", + model: "gpt-4o", + modelOverride: "claude-sonnet-4", + }); + + expect(mockSetModel).toHaveBeenCalledWith("claude-sonnet-4"); + expect(result.model).toBe("claude-sonnet-4"); + expect(result.sessionId).toBe("sess-new"); + }); + + it("calls setModel when modelOverride is provided on resumed session", async () => { + const result = await runCopilotAgent({ + prompt: "hi", + model: "gpt-4o", + modelOverride: "claude-sonnet-4", + sessionId: "existing-session", + }); + + expect(mockResumeSession).toHaveBeenCalledWith("existing-session", expect.any(Object)); + expect(mockSetModel).toHaveBeenCalledWith("claude-sonnet-4"); + expect(result.model).toBe("claude-sonnet-4"); + expect(result.sessionId).toBe("sess-resumed"); + }); + + it("returns model as undefined when no model or override specified", async () => { + const result = await runCopilotAgent({ + prompt: "hi", + }); + + expect(mockSetModel).not.toHaveBeenCalled(); + expect(result.model).toBeUndefined(); + }); +}); diff --git a/src/agents/copilot-sdk.ts b/src/agents/copilot-sdk.ts index 067dffbb9c0b9..6616796d4a6d2 100644 --- a/src/agents/copilot-sdk.ts +++ b/src/agents/copilot-sdk.ts @@ -112,6 +112,8 @@ export async function listCopilotModels(options?: { cwd?: string }): Promise