Skip to content

Commit 41921bf

Browse files
Use new Copilot CLI SDK (#1761)
* Update to latest Copilot CLI SDK * Skip failing testi * Fix test and address review comments * Address review comments * Improve file write concurrency for node pty shim. * Updates * Fix testes --------- Co-authored-by: Peng Lyu <[email protected]>
1 parent fc00af8 commit 41921bf

13 files changed

+1328
-876
lines changed

package-lock.json

Lines changed: 7 additions & 494 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4569,7 +4569,7 @@
45694569
"dependencies": {
45704570
"@anthropic-ai/claude-code": "^1.0.120",
45714571
"@anthropic-ai/sdk": "^0.68.0",
4572-
"@github/copilot": "^0.0.343",
4572+
"@github/copilot": "^0.0.354",
45734573
"@google/genai": "^1.22.0",
45744574
"@humanwhocodes/gitignore-to-minimatch": "1.0.2",
45754575
"@microsoft/tiktokenizer": "^1.0.10",

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

Lines changed: 102 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,66 +3,50 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { ModelProvider } from '@github/copilot/sdk';
6+
import type { SessionOptions } from '@github/copilot/sdk';
77
import type { ChatSessionProviderOptionItem } from 'vscode';
8+
import { IAuthenticationService } from '../../../../platform/authentication/common/authentication';
89
import { IEnvService } from '../../../../platform/env/common/envService';
910
import { IVSCodeExtensionContext } from '../../../../platform/extContext/common/extensionContext';
1011
import { ILogService } from '../../../../platform/log/common/logService';
12+
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
1113
import { createServiceIdentifier } from '../../../../util/common/services';
1214
import { Lazy } from '../../../../util/vs/base/common/lazy';
15+
import { Disposable, IDisposable, toDisposable } from '../../../../util/vs/base/common/lifecycle';
16+
import { getCopilotLogger } from './logger';
1317
import { ensureNodePtyShim } from './nodePtyShim';
1418

1519
const COPILOT_CLI_MODEL_MEMENTO_KEY = 'github.copilot.cli.sessionModel';
16-
const DEFAULT_CLI_MODEL: ModelProvider = {
17-
type: 'anthropic',
18-
model: 'claude-sonnet-4.5'
19-
};
20-
21-
/**
22-
* Convert a model ID to a ModelProvider object for the Copilot CLI SDK
23-
*/
24-
export function getModelProvider(modelId: string): ModelProvider {
25-
// Keep logic minimal; advanced mapping handled by resolveModelProvider in modelMapping.ts.
26-
if (modelId.startsWith('claude-')) {
27-
return {
28-
type: 'anthropic',
29-
model: modelId
30-
};
31-
} else if (modelId.startsWith('gpt-')) {
32-
return {
33-
type: 'openai',
34-
model: modelId
35-
};
36-
}
37-
return DEFAULT_CLI_MODEL;
38-
}
20+
const DEFAULT_CLI_MODEL = 'claude-sonnet-4';
3921

4022
export interface ICopilotCLIModels {
4123
_serviceBrand: undefined;
42-
toModelProvider(modelId: string): ModelProvider;
24+
toModelProvider(modelId: string): string;
4325
getDefaultModel(): Promise<ChatSessionProviderOptionItem>;
4426
setDefaultModel(model: ChatSessionProviderOptionItem): Promise<void>;
4527
getAvailableModels(): Promise<ChatSessionProviderOptionItem[]>;
4628
}
4729

30+
export const ICopilotCLISDK = createServiceIdentifier<ICopilotCLISDK>('ICopilotCLISDK');
31+
4832
export const ICopilotCLIModels = createServiceIdentifier<ICopilotCLIModels>('ICopilotCLIModels');
4933

5034
export class CopilotCLIModels implements ICopilotCLIModels {
5135
declare _serviceBrand: undefined;
5236
private readonly _availableModels: Lazy<Promise<ChatSessionProviderOptionItem[]>>;
5337
constructor(
38+
@ICopilotCLISDK private readonly copilotCLISDK: ICopilotCLISDK,
5439
@IVSCodeExtensionContext private readonly extensionContext: IVSCodeExtensionContext,
5540
) {
5641
this._availableModels = new Lazy<Promise<ChatSessionProviderOptionItem[]>>(() => this._getAvailableModels());
5742
}
5843
public toModelProvider(modelId: string) {
59-
// TODO: replace with SDK-backed lookup once dynamic model list available.
60-
return getModelProvider(modelId);
44+
return modelId;
6145
}
6246
public async getDefaultModel() {
6347
// We control this
6448
const models = await this.getAvailableModels();
65-
const defaultModel = models.find(m => m.id.toLowerCase().includes(DEFAULT_CLI_MODEL.model.toLowerCase())) ?? models[0];
49+
const defaultModel = models.find(m => m.id.toLowerCase() === DEFAULT_CLI_MODEL.toLowerCase()) ?? models[0];
6650
const preferredModelId = this.extensionContext.globalState.get<string>(COPILOT_CLI_MODEL_MEMENTO_KEY, defaultModel.id);
6751

6852
return models.find(m => m.id === preferredModelId) ?? defaultModel;
@@ -78,22 +62,12 @@ export class CopilotCLIModels implements ICopilotCLIModels {
7862
}
7963

8064
private async _getAvailableModels(): Promise<ChatSessionProviderOptionItem[]> {
81-
return [{
82-
id: 'claude-sonnet-4.5',
83-
name: 'Claude Sonnet 4.5'
84-
},
85-
{
86-
id: 'claude-sonnet-4',
87-
name: 'Claude Sonnet 4'
88-
},
89-
{
90-
id: 'claude-haiku-4.5',
91-
name: 'Claude Haiku 4.5'
92-
},
93-
{
94-
id: 'gpt-5',
95-
name: 'GPT-5'
96-
}];
65+
const { getAvailableModels } = await this.copilotCLISDK.getPackage();
66+
const models = await getAvailableModels();
67+
return models.map(model => ({
68+
id: model.model,
69+
name: model.label
70+
} satisfies ChatSessionProviderOptionItem));
9771
}
9872
}
9973

@@ -106,8 +80,6 @@ export interface ICopilotCLISDK {
10680
getPackage(): Promise<typeof import('@github/copilot/sdk')>;
10781
}
10882

109-
export const ICopilotCLISDK = createServiceIdentifier<ICopilotCLISDK>('ICopilotCLISDK');
110-
11183
export class CopilotCLISDK implements ICopilotCLISDK {
11284
declare _serviceBrand: undefined;
11385

@@ -120,11 +92,94 @@ export class CopilotCLISDK implements ICopilotCLISDK {
12092
public async getPackage(): Promise<typeof import('@github/copilot/sdk')> {
12193
try {
12294
// Ensure the node-pty shim exists before importing the SDK (required for CLI sessions)
123-
await ensureNodePtyShim(this.extensionContext.extensionPath, this.envService.appRoot);
95+
await ensureNodePtyShim(this.extensionContext.extensionPath, this.envService.appRoot, this.logService);
12496
return await import('@github/copilot/sdk');
12597
} catch (error) {
12698
this.logService.error(`[CopilotCLISDK] Failed to load @github/copilot/sdk: ${error}`);
12799
throw error;
128100
}
129101
}
130102
}
103+
104+
export interface ICopilotCLISessionOptionsService {
105+
readonly _serviceBrand: undefined;
106+
createOptions(
107+
options: SessionOptions,
108+
permissionHandler: CopilotCLIPermissionsHandler
109+
): Promise<SessionOptions>;
110+
}
111+
export const ICopilotCLISessionOptionsService = createServiceIdentifier<ICopilotCLISessionOptionsService>('ICopilotCLISessionOptionsService');
112+
113+
export class CopilotCLISessionOptionsService implements ICopilotCLISessionOptionsService {
114+
declare _serviceBrand: undefined;
115+
constructor(
116+
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
117+
@IAuthenticationService private readonly _authenticationService: IAuthenticationService,
118+
@ILogService private readonly logService: ILogService,
119+
) { }
120+
121+
public async createOptions(options: SessionOptions, permissionHandler: CopilotCLIPermissionsHandler) {
122+
const copilotToken = await this._authenticationService.getAnyGitHubSession();
123+
const workingDirectory = options.workingDirectory ?? await this.getWorkspaceFolderPath();
124+
const allOptions: SessionOptions = {
125+
env: {
126+
...process.env,
127+
COPILOTCLI_DISABLE_NONESSENTIAL_TRAFFIC: '1'
128+
},
129+
logger: getCopilotLogger(this.logService),
130+
requestPermission: async (permissionRequest) => {
131+
return await permissionHandler.getPermissions(permissionRequest);
132+
},
133+
authInfo: {
134+
type: 'token',
135+
token: copilotToken?.accessToken ?? '',
136+
host: 'https://github.com'
137+
},
138+
...options,
139+
};
140+
141+
if (workingDirectory) {
142+
allOptions.workingDirectory = workingDirectory;
143+
}
144+
return allOptions;
145+
}
146+
private async getWorkspaceFolderPath() {
147+
if (this.workspaceService.getWorkspaceFolders().length === 0) {
148+
return undefined;
149+
}
150+
if (this.workspaceService.getWorkspaceFolders().length === 1) {
151+
return this.workspaceService.getWorkspaceFolders()[0].fsPath;
152+
}
153+
const folder = await this.workspaceService.showWorkspaceFolderPicker();
154+
return folder?.uri?.fsPath;
155+
}
156+
}
157+
158+
159+
/**
160+
* Perhaps temporary interface to handle permission requests from the Copilot CLI SDK
161+
* Perhaps because the SDK needs a better way to handle this in long term per session.
162+
*/
163+
export interface ICopilotCLIPermissions {
164+
onDidRequestPermissions(handler: SessionOptions['requestPermission']): IDisposable;
165+
}
166+
167+
export class CopilotCLIPermissionsHandler extends Disposable implements ICopilotCLIPermissions {
168+
private _handler: SessionOptions['requestPermission'] | undefined;
169+
170+
public onDidRequestPermissions(handler: SessionOptions['requestPermission']): IDisposable {
171+
this._handler = handler;
172+
return this._register(toDisposable(() => {
173+
this._handler = undefined;
174+
}));
175+
}
176+
177+
public async getPermissions(permission: Parameters<NonNullable<SessionOptions['requestPermission']>>[0]): Promise<ReturnType<NonNullable<SessionOptions['requestPermission']>>> {
178+
if (!this._handler) {
179+
return {
180+
kind: "denied-interactively-by-user"
181+
};
182+
}
183+
return await this._handler(permission);
184+
}
185+
}

0 commit comments

Comments
 (0)