Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
553 changes: 196 additions & 357 deletions package-lock.json

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions packages/ai-copilot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,41 @@ For GitHub Enterprise users, configure the enterprise URL via the `ai-features.c
}
```

### Experimental: Copilot SDK transport (`ai-features.copilot.useSdk`)

By default the extension talks directly to the GitHub Copilot REST API. As an
**opt-in experiment**, it can instead delegate to the official Copilot CLI via
the [`@github/copilot-sdk`](https://github.com/github/copilot-sdk) package by
enabling the `ai-features.copilot.useSdk` preference:

```json
{
"ai-features.copilot.useSdk": true
}
```

Because the SDK is a *recognized* GitHub integration, this mode reports the full
current model lineup (instead of only the baseline set such as GPT-4o that the
raw REST integration is entitled to). Toggling the preference rebuilds the
Copilot model set, so no restart is required.

This mode is a prototype and has known limitations:

- **Backend / Node only.** The SDK spawns the bundled `@github/copilot` CLI as a
subprocess over stdio, so it runs in the Theia backend.
- **Single-turn, tools disabled.** The CLI owns its own agent loop, which is
incompatible with Theia's model-as-a-function contract where Theia drives the
tool loop. To keep behaviour predictable the session is created with no tools
and all permission requests are rejected, so requests are effectively
single-turn chat completions.
- **Lossy history.** Multi-message histories (including tool-use/tool-result
turns) are flattened into a single prompt string; thinking messages are
dropped.
- **No structured output.** Structured/JSON output is not supported in this mode.
- **One CLI process per connection.** Each frontend connection currently spawns
its own CLI process; this is acceptable for prototyping but would need a
shared, per-user client for multi-user backends.

### Commands

- **Copilot: Sign In** - Initiates the OAuth device flow authentication
Expand Down
1 change: 1 addition & 0 deletions packages/ai-copilot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "1.72.0",
"description": "Theia - GitHub Copilot Integration",
"dependencies": {
"@github/copilot-sdk": "^1.0.0",
"@theia/ai-core": "1.72.0",
"@theia/ai-openai": "1.72.0",
"@theia/core": "1.72.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { CommandService, nls, PreferenceService } from '@theia/core';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { MessageService } from '@theia/core/lib/common/message-service';
import { CopilotAuthService, CopilotLanguageModelsManager, CopilotModelDescription, COPILOT_PROVIDER_ID } from '../common';
import { COPILOT_ENABLED_PREF, COPILOT_MODEL_OVERRIDES_PREF, COPILOT_ENTERPRISE_URL_PREF } from '../common/copilot-preferences';
import { COPILOT_ENABLED_PREF, COPILOT_MODEL_OVERRIDES_PREF, COPILOT_ENTERPRISE_URL_PREF, COPILOT_USE_SDK_PREF } from '../common/copilot-preferences';
import { AICorePreferences, PREFERENCE_NAME_MAX_RETRIES } from '@theia/ai-core/lib/common/ai-core-preferences';
import { CopilotCommands } from './copilot-command-contribution';

Expand Down Expand Up @@ -51,6 +51,7 @@ export class CopilotFrontendApplicationContribution implements FrontendApplicati
this.preferenceService.ready.then(async () => {
const enterpriseUrl = this.preferenceService.get<string>(COPILOT_ENTERPRISE_URL_PREF);
this.manager.setEnterpriseUrl(enterpriseUrl || undefined);
this.manager.setUseSdk(this.preferenceService.get<boolean>(COPILOT_USE_SDK_PREF, false));

if (this.isCopilotEnabled()) {
const authState = await this.authService.getAuthState();
Expand Down Expand Up @@ -86,6 +87,11 @@ export class CopilotFrontendApplicationContribution implements FrontendApplicati
} else {
this.manager.refreshModelsStatus();
}
} else if (event.preferenceName === COPILOT_USE_SDK_PREF && this.isCopilotEnabled()) {
// Switching transport changes the model implementation, so rebuild the model set.
this.manager.setUseSdk(this.preferenceService.get<boolean>(COPILOT_USE_SDK_PREF, false));
this.removeAllCopilotModels();
this.initializeModels();
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ export interface CopilotLanguageModelsManager {
* Set the GitHub Enterprise URL for Copilot API requests.
*/
setEnterpriseUrl(url: string | undefined): void;
/**
* Enable or disable routing through the official Copilot CLI via `@github/copilot-sdk`
* instead of the direct REST API.
*/
setUseSdk(useSdk: boolean): void;
/**
* Refresh the status of all Copilot models (e.g., after authentication state changes).
*/
Expand Down
13 changes: 13 additions & 0 deletions packages/ai-copilot/src/common/copilot-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { nls, PreferenceSchema } from '@theia/core';
export const COPILOT_ENABLED_PREF = 'ai-features.copilot.enabled';
export const COPILOT_MODEL_OVERRIDES_PREF = 'ai-features.copilot.modelOverrides';
export const COPILOT_ENTERPRISE_URL_PREF = 'ai-features.copilot.enterpriseUrl';
export const COPILOT_USE_SDK_PREF = 'ai-features.copilot.useSdk';

export const CopilotPreferencesSchema: PreferenceSchema = {
properties: {
Expand Down Expand Up @@ -49,6 +50,18 @@ export const CopilotPreferencesSchema: PreferenceSchema = {
'GitHub Enterprise domain for Copilot API (e.g., `github.mycompany.com`). Leave empty for GitHub.com.'),
title: AI_CORE_PREFERENCES_TITLE,
default: ''
},
[COPILOT_USE_SDK_PREF]: {
type: 'boolean',
markdownDescription: nls.localize('theia/ai/copilot/useSdk/mdDescription',
'Experimental: route GitHub Copilot through the official Copilot CLI via the `@github/copilot-sdk` instead of '
+ 'the direct REST API. This makes the full, up-to-date model lineup available (the direct REST integration only '
+ 'exposes a baseline set such as GPT-4o). The Copilot CLI is launched as a background process and authenticated '
+ 'with your existing Copilot sign-in.\n\n'
+ '**Known prototype limitations:** chat is handled as single-turn requests, tool calling is disabled, and '
+ 'structured output is not supported on this path. Leave disabled to use the direct REST integration.'),
title: AI_CORE_PREFERENCES_TITLE,
default: false
}
}
};
3 changes: 3 additions & 0 deletions packages/ai-copilot/src/node/copilot-backend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@ import {
} from '../common';
import { CopilotOAuthConfig, DEFAULT_COPILOT_OAUTH_CONFIG } from '../common/copilot-oauth-config';
import { CopilotLanguageModelsManagerImpl } from './copilot-language-models-manager-impl';
import { CopilotSdkClientProvider } from './copilot-sdk-client-provider';
import { CopilotAuthServiceImpl } from './copilot-auth-service-impl';

const copilotConnectionModule = ConnectionContainerModule.create(({ bind }) => {
bind(CopilotAuthServiceImpl).toSelf().inSingletonScope();
bind(CopilotAuthService).toService(CopilotAuthServiceImpl);

bind(CopilotSdkClientProvider).toSelf().inSingletonScope();

bind(CopilotLanguageModelsManagerImpl).toSelf().inSingletonScope();
bind(CopilotLanguageModelsManager).toService(CopilotLanguageModelsManagerImpl);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { LanguageModelRegistry, LanguageModelStatus } from '@theia/ai-core';
import { LanguageModel, LanguageModelRegistry, LanguageModelStatus } from '@theia/ai-core';
import { Disposable, DisposableCollection } from '@theia/core';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { CopilotLanguageModelsManager, CopilotModelDescription, COPILOT_PROVIDER_ID, getCopilotApiBaseUrl } from '../common';
import { CopilotOAuthConfig } from '../common/copilot-oauth-config';
import { CopilotLanguageModel } from './copilot-language-model';
import { CopilotSdkLanguageModel } from './copilot-sdk-language-model';
import { CopilotSdkClientProvider } from './copilot-sdk-client-provider';
import { CopilotAuthServiceImpl } from './copilot-auth-service-impl';

/**
Expand All @@ -38,7 +40,11 @@ export class CopilotLanguageModelsManagerImpl implements CopilotLanguageModelsMa
@inject(CopilotOAuthConfig)
protected readonly oauthConfig: CopilotOAuthConfig;

@inject(CopilotSdkClientProvider)
protected readonly sdkClientProvider: CopilotSdkClientProvider;

protected enterpriseUrl: string | undefined;
protected useSdk = false;
protected readonly toDispose = new DisposableCollection();

@postConstruct()
Expand All @@ -56,6 +62,17 @@ export class CopilotLanguageModelsManagerImpl implements CopilotLanguageModelsMa
this.enterpriseUrl = url;
}

setUseSdk(useSdk: boolean): void {
if (this.useSdk === useSdk) {
return;
}
this.useSdk = useSdk;
// Release any running CLI process when leaving SDK mode.
if (!useSdk) {
this.sdkClientProvider.reset();
}
}

protected async calculateStatus(): Promise<LanguageModelStatus> {
const authState = await this.authService.getAuthState();
if (authState.isAuthenticated) {
Expand All @@ -71,35 +88,53 @@ export class CopilotLanguageModelsManagerImpl implements CopilotLanguageModelsMa
const model = await this.languageModelRegistry.getLanguageModel(modelDescription.id);

if (model) {
if (!(model instanceof CopilotLanguageModel)) {
if (model instanceof CopilotSdkLanguageModel) {
await this.languageModelRegistry.patchLanguageModel<CopilotSdkLanguageModel>(modelDescription.id, {
model: modelDescription.model,
status,
maxRetries: modelDescription.maxRetries
});
} else if (model instanceof CopilotLanguageModel) {
await this.languageModelRegistry.patchLanguageModel<CopilotLanguageModel>(modelDescription.id, {
model: modelDescription.model,
enableStreaming: modelDescription.enableStreaming,
supportsStructuredOutput: modelDescription.supportsStructuredOutput,
status,
maxRetries: modelDescription.maxRetries
});
} else {
console.warn(`Copilot: model ${modelDescription.id} is not a Copilot model`);
continue;
}
await this.languageModelRegistry.patchLanguageModel<CopilotLanguageModel>(modelDescription.id, {
model: modelDescription.model,
enableStreaming: modelDescription.enableStreaming,
supportsStructuredOutput: modelDescription.supportsStructuredOutput,
status,
maxRetries: modelDescription.maxRetries
});
} else {
this.languageModelRegistry.addLanguageModels([
new CopilotLanguageModel(
modelDescription.id,
modelDescription.model,
status,
modelDescription.enableStreaming,
modelDescription.supportsStructuredOutput,
modelDescription.maxRetries,
() => this.authService.getAccessToken(),
() => this.enterpriseUrl,
() => this.oauthConfig.userAgent
)
]);
this.languageModelRegistry.addLanguageModels([this.createLanguageModel(modelDescription, status)]);
}
}
}

protected createLanguageModel(modelDescription: CopilotModelDescription, status: LanguageModelStatus): LanguageModel {
if (this.useSdk) {
return new CopilotSdkLanguageModel(
modelDescription.id,
modelDescription.model,
status,
modelDescription.maxRetries,
() => this.sdkClientProvider.getClient()
);
}
return new CopilotLanguageModel(
modelDescription.id,
modelDescription.model,
status,
modelDescription.enableStreaming,
modelDescription.supportsStructuredOutput,
modelDescription.maxRetries,
() => this.authService.getAccessToken(),
() => this.enterpriseUrl,
() => this.oauthConfig.userAgent
);
}

removeLanguageModels(...modelIds: string[]): void {
this.languageModelRegistry.removeLanguageModels(modelIds);
}
Expand All @@ -109,7 +144,7 @@ export class CopilotLanguageModelsManagerImpl implements CopilotLanguageModelsMa
const allModels = await this.languageModelRegistry.getLanguageModels();

for (const model of allModels) {
if (model instanceof CopilotLanguageModel && model.id.startsWith(`${COPILOT_PROVIDER_ID}/`)) {
if ((model instanceof CopilotLanguageModel || model instanceof CopilotSdkLanguageModel) && model.id.startsWith(`${COPILOT_PROVIDER_ID}/`)) {
await this.languageModelRegistry.patchLanguageModel<CopilotLanguageModel>(model.id, {
status
});
Expand All @@ -118,6 +153,15 @@ export class CopilotLanguageModelsManagerImpl implements CopilotLanguageModelsMa
}

async fetchAvailableModelIds(): Promise<string[]> {
if (this.useSdk) {
try {
return await this.sdkClientProvider.listModelIds();
} catch (error) {
console.warn('Copilot: failed to fetch available models via the Copilot SDK:', error);
return [];
}
}

const accessToken = await this.authService.getAccessToken();
if (!accessToken) {
return [];
Expand Down
Loading