Skip to content

Commit c823402

Browse files
committed
feat: add reasoning effort configuration for Copilot CLI
1 parent d186742 commit c823402

12 files changed

Lines changed: 214 additions & 76 deletions

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4573,6 +4573,14 @@
45734573
"advanced"
45744574
]
45754575
},
4576+
"github.copilot.chat.cli.thinkingEffort.enabled": {
4577+
"type": "boolean",
4578+
"default": true,
4579+
"markdownDescription": "%github.copilot.config.cli.thinkingEffort.enabled%",
4580+
"tags": [
4581+
"advanced"
4582+
]
4583+
},
45764584
"github.copilot.chat.cli.sessionControllerForSessionsApp.enabled": {
45774585
"type": "boolean",
45784586
"default": false,

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@
407407
"github.copilot.config.cli.isolationOption.enabled": "Enable the isolation mode option for Copilot CLI. When enabled, users can choose between Worktree and Workspace modes.",
408408
"github.copilot.config.cli.autoCommit.enabled": "Enable automatic commit for Copilot CLI. When enabled, changes made by Copilot CLI will be automatically committed to the repository at the end of each turn.",
409409
"github.copilot.config.cli.sessionController.enabled": "Enable the new session controller API for Copilot CLI. Requires VS Code reload.",
410+
"github.copilot.config.cli.thinkingEffort.enabled": "Enable thinking effort for Language Models in Copilot CLI.",
410411
"github.copilot.config.cli.sessionControllerForSessionsApp.enabled": "Enable the new session controller API for Sessions App. Requires VS Code reload.",
411412
"github.copilot.config.cli.terminalLinks.enabled": "Enable advanced clickable file links in Copilot CLI terminals. Resolves relative paths against session state directories. Requires VS Code reload.",
412413
"github.copilot.config.backgroundAgent.enabled": "Enable the Copilot CLI. When disabled, the Copilot CLI will not be available in 'Continue In' context menus.",

src/extension/chatSessions/copilotcli/node/copilotCli.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import type { SessionOptions, SweCustomAgent } from '@github/copilot/sdk';
7+
import * as l10n from '@vscode/l10n';
78
import { promises as fs } from 'fs';
89
import * as path from 'path';
910
import type * as vscode from 'vscode';
@@ -26,6 +27,7 @@ import { getCopilotLogger } from './logger';
2627
import { ensureNodePtyShim } from './nodePtyShim';
2728
import { ensureRipgrepShim } from './ripgrepShim';
2829

30+
export const COPILOT_CLI_REASONING_EFFORT_PROPERTY = 'reasoningEffort';
2931
const COPILOT_CLI_MODEL_MEMENTO_KEY = 'github.copilot.cli.sessionModel';
3032
const COPILOT_CLI_REQUEST_MAP_KEY = 'github.copilot.cli.requestMap';
3133
// Store last used Agent for a Session.
@@ -44,6 +46,9 @@ export interface CopilotCLIModelInfo {
4446
readonly maxOutputTokens?: number;
4547
readonly maxContextWindowTokens: number;
4648
readonly supportsVision?: boolean;
49+
readonly supportsReasoningEffort?: boolean;
50+
readonly defaultReasoningEffort?: string;
51+
readonly supportedReasoningEfforts?: string[];
4752
}
4853

4954
export interface ICopilotCLIModels {
@@ -124,6 +129,9 @@ export class CopilotCLIModels extends Disposable implements ICopilotCLIModels {
124129
maxOutputTokens: model.capabilities.limits.max_output_tokens,
125130
maxContextWindowTokens: model.capabilities.limits.max_context_window_tokens,
126131
supportsVision: model.capabilities.supports.vision,
132+
supportsReasoningEffort: model.capabilities.supports.reasoningEffort,
133+
defaultReasoningEffort: model.defaultReasoningEffort,
134+
supportedReasoningEfforts: model.supportedReasoningEfforts
127135
} satisfies CopilotCLIModelInfo));
128136
} catch (ex) {
129137
this.logService.error(`[CopilotCLISession] Failed to fetch models`, ex);
@@ -152,7 +160,7 @@ export class CopilotCLIModels extends Disposable implements ICopilotCLIModels {
152160

153161
private async _provideLanguageModelChatInfo(): Promise<vscode.LanguageModelChatInformation[]> {
154162
const models = await this.getModels();
155-
return models.map((model, index) => {
163+
const modelsInfo = models.map((model, index) => {
156164
const multiplier = model.multiplier === undefined ? undefined : `${model.multiplier}x`;
157165
return {
158166
id: model.id,
@@ -164,14 +172,16 @@ export class CopilotCLIModels extends Disposable implements ICopilotCLIModels {
164172
multiplier,
165173
multiplierNumeric: model.multiplier,
166174
isUserSelectable: true,
175+
configurationSchema: buildConfigurationSchema(model),
167176
capabilities: {
168177
imageInput: model.supportsVision,
169-
toolCalling: true
178+
toolCalling: true,
170179
},
171180
targetChatSessionType: 'copilotcli',
172181
isDefault: index === 0 // SDK guarantees the first item is the default model
173182
};
174183
});
184+
return modelsInfo;
175185
}
176186
}
177187

@@ -181,6 +191,37 @@ export interface CLIAgentInfo {
181191
/** File URI for prompt-file agents, synthetic `copilotcli:` URI for SDK-only agents. */
182192
readonly sourceUri: URI;
183193
}
194+
function buildConfigurationSchema(modelInfo: CopilotCLIModelInfo): vscode.LanguageModelConfigurationSchema | undefined {
195+
const effortLevels = modelInfo.supportedReasoningEfforts ?? [];
196+
if (effortLevels.length === 0) {
197+
return;
198+
}
199+
200+
const defaultEffort = modelInfo.defaultReasoningEffort;
201+
202+
return {
203+
properties: {
204+
[COPILOT_CLI_REASONING_EFFORT_PROPERTY]: {
205+
type: 'string',
206+
title: l10n.t('Thinking Effort'),
207+
enum: effortLevels,
208+
enumItemLabels: effortLevels.map(level => level.charAt(0).toUpperCase() + level.slice(1)),
209+
enumDescriptions: effortLevels.map(level => {
210+
switch (level) {
211+
case 'none': return l10n.t('No reasoning applied');
212+
case 'low': return l10n.t('Faster responses with less reasoning');
213+
case 'medium': return l10n.t('Balanced reasoning and speed');
214+
case 'high': return l10n.t('Greater reasoning depth but slower');
215+
case 'xhigh': return l10n.t('Maximum reasoning depth but slower');
216+
default: return level;
217+
}
218+
}),
219+
default: defaultEffort,
220+
group: 'navigation',
221+
}
222+
}
223+
};
224+
}
184225

185226
export interface ICopilotCLIAgents {
186227
readonly _serviceBrand: undefined;

src/extension/chatSessions/copilotcli/node/copilotcliSession.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export interface ICopilotCLISession extends IDisposable {
8686
request: { id: string; toolInvocationToken: ChatParticipantToolToken; sessionResource?: vscode.Uri },
8787
input: CopilotCLISessionInput,
8888
attachments: Attachment[],
89-
modelId: string | undefined,
89+
model: { model: string; reasoningEffort?: string } | undefined,
9090
authInfo: NonNullable<SessionOptions['authInfo']>,
9191
token: vscode.CancellationToken
9292
): Promise<void>;
@@ -213,7 +213,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
213213
request: { id: string; toolInvocationToken: ChatParticipantToolToken; sessionResource?: vscode.Uri },
214214
input: CopilotCLISessionInput,
215215
attachments: Attachment[],
216-
modelId: string | undefined,
216+
model: { model: string; reasoningEffort?: string } | undefined,
217217
authInfo: NonNullable<SessionOptions['authInfo']>,
218218
token: vscode.CancellationToken
219219
): Promise<void> {
@@ -229,12 +229,12 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
229229
const previousRequestSnapshot = this.previousRequest;
230230

231231
const handled = this._requestLogger.captureInvocation(capturingToken, async () => {
232-
await this.updateModel(modelId, authInfo, token);
232+
await this.updateModel(model?.model, model?.reasoningEffort, authInfo, token);
233233

234234
if (isAlreadyBusyWithAnotherRequest) {
235-
return this._handleRequestSteering(input, attachments, modelId, previousRequestSnapshot, token);
235+
return this._handleRequestSteering(input, attachments, model, previousRequestSnapshot, token);
236236
} else {
237-
return this._handleRequestImpl(request, input, attachments, modelId, token);
237+
return this._handleRequestImpl(request, input, attachments, model, token);
238238
}
239239
});
240240

@@ -261,7 +261,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
261261
private async _handleRequestSteering(
262262
input: CopilotCLISessionInput,
263263
attachments: Attachment[],
264-
modelId: string | undefined,
264+
model: { model: string; reasoningEffort?: string } | undefined,
265265
previousRequestPromise: Promise<unknown>,
266266
token: vscode.CancellationToken,
267267
): Promise<void> {
@@ -281,9 +281,9 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
281281
// previous request to finish, so this promise settles only once all
282282
// in-flight work is done.
283283
await Promise.all([previousRequestPromise, this.sendRequestInternal(input, attachments, true, logStartTime)]);
284-
this._logConversation(prompt, '', modelId || '', attachments, logStartTime, 'Completed');
284+
this._logConversation(prompt, '', model?.model || '', attachments, logStartTime, 'Completed');
285285
} catch (error) {
286-
this._logConversation(prompt, '', modelId || '', attachments, logStartTime, 'Failed', error instanceof Error ? error.message : String(error));
286+
this._logConversation(prompt, '', model?.model || '', attachments, logStartTime, 'Failed', error instanceof Error ? error.message : String(error));
287287
throw error;
288288
} finally {
289289
disposables.dispose();
@@ -294,9 +294,10 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
294294
request: { id: string; toolInvocationToken: ChatParticipantToolToken },
295295
input: CopilotCLISessionInput,
296296
attachments: Attachment[],
297-
modelId: string | undefined,
297+
model: { model: string; reasoningEffort?: string } | undefined,
298298
token: vscode.CancellationToken
299299
): Promise<void> {
300+
const modelId = model?.model;
300301
return this._otelService.startActiveSpan(
301302
'invoke_agent copilotcli',
302303
{
@@ -766,7 +767,7 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
766767
}
767768
}
768769

769-
private async updateModel(modelId: string | undefined, authInfo: NonNullable<SessionOptions['authInfo']>, token: CancellationToken): Promise<void> {
770+
private async updateModel(modelId: string | undefined, reasoningEffort: string | undefined, authInfo: NonNullable<SessionOptions['authInfo']>, token: CancellationToken): Promise<void> {
770771
// Where possible try to avoid an extra call to getSelectedModel by using cached value.
771772
let currentModel: string | undefined = undefined;
772773
if (modelId) {
@@ -782,9 +783,13 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
782783
if (authInfo) {
783784
this._sdkSession.setAuthInfo(authInfo);
784785
}
785-
if (modelId && modelId !== currentModel) {
786-
this._lastUsedModel = modelId;
787-
await raceCancellation(this._sdkSession.setSelectedModel(modelId), token);
786+
if (modelId) {
787+
if (modelId !== currentModel) {
788+
this._lastUsedModel = modelId;
789+
await raceCancellation(this._sdkSession.setSelectedModel(modelId, reasoningEffort), token);
790+
} else if (reasoningEffort && this._sdkSession.getReasoningEffort() !== reasoningEffort) {
791+
await raceCancellation(this._sdkSession.setSelectedModel(modelId, reasoningEffort), token);
792+
}
788793
}
789794
}
790795

src/extension/chatSessions/copilotcli/node/copilotcliSessionService.ts

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export interface ICopilotCLISessionItem {
6565
export type ExtendedChatRequest = ChatRequest & { prompt: string };
6666
export type ISessionOptions = {
6767
model?: string;
68+
reasoningEffort?: string;
6869
workspace: IWorkspaceInfo;
6970
agent?: SweCustomAgent;
7071
debugTargetSessionIds?: readonly string[];
@@ -526,34 +527,33 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
526527
}
527528
}
528529

529-
public async createSession({ model, workspace, agent, sessionId, debugTargetSessionIds, mcpServerMappings }: ICreateSessionOptions, token: CancellationToken): Promise<RefCountedSession> {
530+
public async createSession(options: ICreateSessionOptions, token: CancellationToken): Promise<RefCountedSession> {
530531
const { mcpConfig: mcpServers, disposable: mcpGateway } = await this.mcpHandler.loadMcpConfig();
531532
try {
532-
const copilotUrl = this.configurationService.getConfig(ConfigKey.Shared.DebugOverrideProxyUrl) || undefined;
533-
const { agentName, sessionOptions } = await this.createSessionsOptions({ model, workspace, mcpServers, agent, copilotUrl, sessionId, debugTargetSessionIds, mcpServerMappings });
533+
const sessionOptions = await this.createSessionsOptions({ ...options, mcpServers });
534534
const sessionManager = await raceCancellationError(this.getSessionManager(), token);
535-
const sdkSession = await sessionManager.createSession({ ...sessionOptions, sessionId });
535+
const sdkSession = await sessionManager.createSession({ ...sessionOptions, sessionId: options.sessionId });
536536
this._newSessionIds.delete(sdkSession.sessionId);
537537
// After the first session creation, the SDK's OTel TracerProvider is
538538
// initialized. Install the bridge processor so SDK-native spans flow
539539
// to the debug panel.
540540
this._installBridgeIfNeeded();
541541

542-
if (copilotUrl) {
542+
if (sessionOptions.copilotUrl) {
543543
sdkSession.setAuthInfo({
544544
type: 'hmac',
545545
hmac: 'empty',
546546
host: 'https://github.com',
547547
copilotUser: {
548548
endpoints: {
549-
api: copilotUrl
549+
api: sessionOptions.copilotUrl
550550
}
551551
}
552552
});
553553
}
554554
this.logService.trace(`[CopilotCLISession] Created new CopilotCLI session ${sdkSession.sessionId}.`);
555555

556-
const session = this.createCopilotSession(sdkSession, workspace, agentName, sessionManager);
556+
const session = this.createCopilotSession(sdkSession, options.workspace, options.agent?.name, sessionManager);
557557
session.object.add(mcpGateway);
558558
return session;
559559
}
@@ -638,7 +638,7 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
638638
return false;
639639
}
640640

641-
protected async createSessionsOptions(options: { model?: string; workspace: IWorkspaceInfo; mcpServers?: SessionOptions['mcpServers']; agent: SweCustomAgent | undefined; copilotUrl?: string; sessionId?: string; debugTargetSessionIds?: readonly string[]; mcpServerMappings?: McpServerMappings }): Promise<{ readonly sessionOptions: Readonly<SessionOptions>; readonly agentName: string | undefined }> {
641+
protected async createSessionsOptions(options: ICreateSessionOptions & { mcpServers?: SessionOptions['mcpServers'] }): Promise<Readonly<SessionOptions>> {
642642
const [agentInfos, skillLocations] = await Promise.all([
643643
this.agents.getAgents(),
644644
this.copilotCLISkills.getSkillsLocations(),
@@ -680,34 +680,35 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
680680
allOptions.customAgents = customAgents;
681681
}
682682
allOptions.enableStreaming = true;
683-
if (options.copilotUrl) {
684-
allOptions.copilotUrl = options.copilotUrl;
683+
const copilotUrl = this.configurationService.getConfig(ConfigKey.Shared.DebugOverrideProxyUrl) || undefined;
684+
if (copilotUrl) {
685+
allOptions.copilotUrl = copilotUrl;
685686
}
686687
if (systemMessage) {
687688
allOptions.systemMessage = systemMessage;
688689
}
689690
allOptions.sessionCapabilities = new Set(['plan-mode', 'memory', 'cli-documentation', 'ask-user', 'interactive-mode', 'system-notifications']);
691+
if (options.reasoningEffort) {
692+
allOptions.reasoningEffort = options.reasoningEffort;
693+
}
690694

691-
return {
692-
sessionOptions: allOptions as Readonly<SessionOptions>,
693-
agentName: options.agent?.name,
694-
};
695+
return allOptions as Readonly<SessionOptions>;
695696
}
696697

697-
public async getSession({ sessionId, model, workspace, agent, debugTargetSessionIds, mcpServerMappings }: IGetSessionOptions, token: CancellationToken): Promise<RefCountedSession | undefined> {
698+
public async getSession(options: IGetSessionOptions, token: CancellationToken): Promise<RefCountedSession | undefined> {
698699
// https://github.com/microsoft/vscode/issues/276573
699-
const lock = this.sessionMutexForGetSession.get(sessionId) ?? new Mutex();
700-
this.sessionMutexForGetSession.set(sessionId, lock);
700+
const lock = this.sessionMutexForGetSession.get(options.sessionId) ?? new Mutex();
701+
this.sessionMutexForGetSession.set(options.sessionId, lock);
701702
const lockDisposable = await lock.acquire(token);
702703
try {
703704
{
704-
const session = this._sessionWrappers.get(sessionId);
705+
const session = this._sessionWrappers.get(options.sessionId);
705706
if (session) {
706-
this.logService.trace(`[CopilotCLISession] Reusing CopilotCLI session ${sessionId}.`);
707-
this._partialSessionHistories.delete(sessionId);
707+
this.logService.trace(`[CopilotCLISession] Reusing CopilotCLI session ${options.sessionId}.`);
708+
this._partialSessionHistories.delete(options.sessionId);
708709
session.acquire();
709-
if (agent) {
710-
await session.object.sdkSession.selectCustomAgent(agent.name);
710+
if (options.agent) {
711+
await session.object.sdkSession.selectCustomAgent(options.agent.name);
711712
} else {
712713
session.object.sdkSession.clearCustomAgent();
713714
}
@@ -720,16 +721,15 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
720721
this.mcpHandler.loadMcpConfig(),
721722
]);
722723
try {
723-
const copilotUrl = this.configurationService.getConfig(ConfigKey.Shared.DebugOverrideProxyUrl) || undefined;
724-
const { agentName, sessionOptions } = await this.createSessionsOptions({ model, agent, workspace: workspace, mcpServers, copilotUrl, sessionId, debugTargetSessionIds, mcpServerMappings });
724+
const sessionOptions = await this.createSessionsOptions({ ...options, mcpServers });
725725

726-
const sdkSession = await sessionManager.getSession({ ...sessionOptions, sessionId }, true);
726+
const sdkSession = await sessionManager.getSession({ ...sessionOptions, sessionId: options.sessionId }, true);
727727
if (!sdkSession) {
728-
this.logService.error(`[CopilotCLISession] CopilotCLI failed to get session ${sessionId}.`);
728+
this.logService.error(`[CopilotCLISession] CopilotCLI failed to get session ${options.sessionId}.`);
729729
return undefined;
730730
}
731731

732-
const session = this.createCopilotSession(sdkSession, workspace, agentName, sessionManager);
732+
const session = this.createCopilotSession(sdkSession, options.workspace, options.agent?.name, sessionManager);
733733
session.object.add(mcpGateway);
734734
return session;
735735
}
@@ -875,12 +875,11 @@ export class CopilotCLISessionService extends Disposable implements ICopilotCLIS
875875
const newSessionId = generateUuid();
876876
this._sessionsBeingCreatedViaFork.add(newSessionId);
877877
try {
878-
const copilotUrl = this.configurationService.getConfig(ConfigKey.Shared.DebugOverrideProxyUrl) || undefined;
879-
const [sessionManager, title, { history, events: originalSessionEvents }, { sessionOptions }] = await Promise.all([
878+
const [sessionManager, title, { history, events: originalSessionEvents }, sessionOptions] = await Promise.all([
880879
raceCancellationError(this.getSessionManager(), token),
881880
this.getSessionTitle(sessionId, token),
882881
requestId ? this.getChatHistoryImpl({ sessionId, workspace }, token) : Promise.resolve({ history: [], events: [] }),
883-
this.createSessionsOptions({ workspace, mcpServers: undefined, copilotUrl, agent: undefined, sessionId: newSessionId }),
882+
this.createSessionsOptions({ workspace, mcpServers: undefined, agent: undefined, sessionId: newSessionId }),
884883
copySessionFilesForForking(sessionId, newSessionId, workspace, this._chatSessionMetadataStore, token),
885884
]);
886885

0 commit comments

Comments
 (0)