Skip to content

Commit f7423b4

Browse files
committed
feat: Add a command to change Copilot models
Also, default to `gpt-4o`. Users can now run `AppMap: Select Copilot Model` to change their Copilot model.
1 parent 9535e34 commit f7423b4

7 files changed

Lines changed: 125 additions & 2 deletions

File tree

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,10 @@
213213
"command": "appmap.rpc.restart",
214214
"title": "AppMap: Restart Navie"
215215
},
216+
{
217+
"command": "appmap.copilot.selectModel",
218+
"title": "AppMap: Select Copilot Model"
219+
},
216220
{
217221
"command": "appmap.login",
218222
"title": "AppMap: Login"
@@ -335,6 +339,10 @@
335339
"type": "boolean",
336340
"default": true,
337341
"description": "Use animations"
342+
},
343+
"appMap.copilot.preferredModel": {
344+
"type": "string",
345+
"description": "Preferred Copilot model to use"
338346
}
339347
}
340348
},

src/commands/pickCopilotModel.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import ExtensionSettings from '../configuration/extensionSettings';
2+
import ChatCompletion from '../services/chatCompletion';
3+
import vscode from 'vscode';
4+
5+
export default class PickCopilotModelCommand {
6+
public static readonly command = 'appmap.copilot.selectModel';
7+
8+
public static register(context: vscode.ExtensionContext): void {
9+
context.subscriptions.push(
10+
vscode.commands.registerCommand(
11+
PickCopilotModelCommand.command,
12+
PickCopilotModelCommand.execute
13+
)
14+
);
15+
}
16+
17+
public static async execute(): Promise<void> {
18+
await ChatCompletion.refreshModels();
19+
if (ChatCompletion.models.length === 0) {
20+
vscode.window.showErrorMessage('No Copilot models are available.');
21+
return;
22+
}
23+
24+
const model = await vscode.window.showQuickPick(
25+
ChatCompletion.models.map((m) => ({ label: m.name, details: m.id }))
26+
);
27+
if (!model) return;
28+
29+
await ExtensionSettings.setPreferredCopilotModel(model.details);
30+
return vscode.commands.executeCommand('appmap.rpc.restart');
31+
}
32+
}

src/configuration/extensionSettings.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ export default class ExtensionSettings {
7979
return ret !== undefined ? ret : 20000;
8080
}
8181

82+
public static get preferredCopilotModel(): string | undefined {
83+
return vscode.workspace.getConfiguration('appMap').get('copilot.preferredModel', 'gpt-4o');
84+
}
85+
86+
public static setPreferredCopilotModel(model: string | undefined): Thenable<void> {
87+
return vscode.workspace.getConfiguration('appMap').update('copilot.preferredModel', model);
88+
}
89+
8290
public static get useAnimation(): boolean {
8391
return [true, 'true'].includes(
8492
vscode.workspace.getConfiguration('appMap').get('useAnimation') || false

src/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import CommandRegistry from './commands/commandRegistry';
6363
import AssetService from './assets/assetService';
6464
import clearNavieAiSettings from './commands/clearNavieAiSettings';
6565
import ExtensionSettings from './configuration/extensionSettings';
66+
import PickCopilotModelCommand from './commands/pickCopilotModel';
6667

6768
export async function activate(context: vscode.ExtensionContext): Promise<AppMapService> {
6869
CommandRegistry.setContext(context).addWaitAlias({
@@ -226,6 +227,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<AppMap
226227
const processService = new NodeProcessService(context);
227228

228229
await ChatCompletion.initialize(context);
230+
PickCopilotModelCommand.register(context);
229231

230232
AssetService.register(context);
231233
const dependenciesInstalled = ExtensionSettings.appMapCommandLineToolsPath

src/services/chatCompletion.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,12 @@ export default class ChatCompletion implements Disposable {
112112
}
113113

114114
static get preferredModel(): vscode.LanguageModelChat | undefined {
115-
return ChatCompletion.models[0];
115+
const modelId = ExtensionSettings.preferredCopilotModel;
116+
if (modelId) {
117+
const model = this.models.find((m) => m.id === modelId);
118+
if (model) return model;
119+
}
120+
return this.models[0];
116121
}
117122

118123
static async refreshModels(): Promise<boolean> {
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import vscode from '../mock/vscode';
2+
import sinon from 'sinon';
3+
import { expect } from 'chai';
4+
import PickCopilotModelCommand from '../../../src/commands/pickCopilotModel';
5+
6+
describe('pickCopilotModel', () => {
7+
describe('execute', () => {
8+
let models: Record<string, unknown>[];
9+
let executeCommandStub: sinon.SinonStub;
10+
let showQuickPickStub: sinon.SinonStub;
11+
let showErrorMessageStub: sinon.SinonStub;
12+
const chosenModel = 'claude-3.5-sonnet';
13+
beforeEach(() => {
14+
models = [
15+
{ id: 'gpt-4o', name: 'GPT-4o', maxInputTokens: 325, family: 'copilot' },
16+
{
17+
id: 'claude-3.5-sonnet',
18+
name: 'Claude 3.5 Sonnet',
19+
maxInputTokens: 325,
20+
family: 'copilot',
21+
},
22+
];
23+
showQuickPickStub = sinon.stub(vscode.window, 'showQuickPick').callsFake(() => ({
24+
details: chosenModel,
25+
}));
26+
showErrorMessageStub = sinon.stub(vscode.window, 'showErrorMessage').resolves();
27+
executeCommandStub = sinon.stub(vscode.commands, 'executeCommand').resolves();
28+
sinon.stub(vscode.lm, 'selectChatModels').callsFake(() => models as any); // eslint-disable-line @typescript-eslint/no-explicit-any
29+
});
30+
31+
afterEach(() => {
32+
sinon.restore();
33+
});
34+
35+
const setPreferredModel = (model: string | undefined) =>
36+
vscode.workspace.getConfiguration('appMap').update('copilot.preferredModel', model);
37+
const getPreferredModel = () =>
38+
vscode.workspace.getConfiguration('appMap').get('copilot.preferredModel');
39+
40+
it('sets the appMap.copilot.preferredModel setting', async () => {
41+
setPreferredModel(undefined);
42+
await PickCopilotModelCommand.execute();
43+
expect(getPreferredModel()).to.equal(chosenModel);
44+
});
45+
46+
it('shows an error message if no models are available', async () => {
47+
models = [];
48+
await PickCopilotModelCommand.execute();
49+
expect(showErrorMessageStub.called).to.be.true;
50+
expect(showQuickPickStub.called).to.be.false;
51+
expect(executeCommandStub.called).to.be.false;
52+
});
53+
54+
it('restarts the RPC server', async () => {
55+
await PickCopilotModelCommand.execute();
56+
expect(executeCommandStub.calledWith('appmap.rpc.restart')).to.be.true;
57+
});
58+
59+
it('does nothing if the user cancels the quick pick', async () => {
60+
showQuickPickStub.resolves(undefined);
61+
setPreferredModel(chosenModel);
62+
await PickCopilotModelCommand.execute();
63+
expect(getPreferredModel()).to.equal(chosenModel);
64+
expect(executeCommandStub.called).to.be.false;
65+
});
66+
});
67+
});

test/unit/mock/vscode/window.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import EventEmitter from './EventEmitter';
44
import Terminal from './Terminal';
55
import type * as vscode from 'vscode';
66

7-
const doNothing = () => {
7+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
8+
const doNothing = (..._args: unknown[]) => {
89
// nop
910
};
1011

0 commit comments

Comments
 (0)