Skip to content
Open
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
26 changes: 25 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"version": "0.32.0",
"build": "1",
"internalAIKey": "1058ec22-3c95-4951-8443-f26c1f325911",
"completionsCore": "74943bef72acfd94931c6ec2c1c7420c8c932336",
"completionsCore": "07be33f7faf935076909fc82bc0f5ac578cca983",
"completionsCoreVersion": "1.372.0",
"internalLargeStorageAriaKey": "ec712b3202c5462fb6877acae7f1f9d7-c19ad55e-3e3c-4f99-984b-827f6d95bd9e-6917",
"ariaKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255",
Expand Down Expand Up @@ -3128,6 +3128,30 @@
"tags": [
"experimental"
]
},
"github.copilot.chat.completionsFetcher": {
"type": ["string", "null"],
"markdownDescription": "%github.copilot.config.completionsFetcher%",
"tags": [
"experimental",
"onExp"
],
"enum": [
"electron-fetch",
"node-fetch"
]
},
"github.copilot.chat.nesFetcher": {
"type": ["string", "null"],
"markdownDescription": "%github.copilot.config.nesFetcher%",
"tags": [
"experimental",
"onExp"
],
"enum": [
"electron-fetch",
"node-fetch"
]
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -305,5 +305,7 @@
"github.copilot.config.useResponsesApi": "Use the Responses API instead of the Chat Completions API when supported. Enables reasoning and reasoning summaries.\n\n**Note**: This is an experimental feature that is not yet activated for all users.",
"github.copilot.config.responsesApiReasoningEffort": "Sets the reasoning effort used for the Responses API. Requires `#github.copilot.chat.useResponsesApi#`.",
"github.copilot.config.responsesApiReasoningSummary": "Sets the reasoning summary style used for the Responses API. Requires `#github.copilot.chat.useResponsesApi#`.",
"github.copilot.config.executePrompt.enabled": "The executePrompt tool enables the agent to execute tasks in a separate, isolated context."
"github.copilot.config.executePrompt.enabled": "The executePrompt tool enables the agent to execute tasks in a separate, isolated context.",
"github.copilot.config.completionsFetcher": "Sets the fetcher used for the inline completions.",
"github.copilot.config.nesFetcher": "Sets the fetcher used for the next edit suggestions."
}
3 changes: 3 additions & 0 deletions src/extension/xtab/node/xtabProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,8 @@ export class XtabProvider implements IStatelessNextEditProvider {
) {
const tracer = parentTracer.sub('streamEdits');

const useFetcher = this.configService.getExperimentBasedConfig(ConfigKey.NextEditSuggestionsFetcher, this.expService) || undefined;

const fetchStreamSource = new FetchStreamSource();

const fetchRequestStopWatch = new StopWatch();
Expand Down Expand Up @@ -531,6 +533,7 @@ export class XtabProvider implements IStatelessNextEditProvider {
telemetryProperties: {
requestId: request.id,
},
useFetcher,
},
cancellationToken,
);
Expand Down
4 changes: 4 additions & 0 deletions src/platform/configuration/common/configurationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ResponseProcessor } from '../../inlineEdits/common/responseProcessor';
import { AlternativeNotebookFormat } from '../../notebook/common/alternativeContentFormat';
import { IExperimentationService } from '../../telemetry/common/nullExperimentationService';
import { IValidator, vBoolean, vString } from './validator';
import { FetcherId } from '../../networking/common/fetcherService';

export const CopilotConfigPrefix = 'github.copilot';

Expand Down Expand Up @@ -811,6 +812,9 @@ export namespace ConfigKey {
export const GrokCodeAlternatePrompt = defineExpSetting<string>('chat.grokCodeAlternatePrompt', 'default');
export const ClaudeSonnet45AlternatePrompt = defineExpSetting<string>('chat.claudeSonnet45AlternatePrompt', 'default');
export const ExecutePromptEnabled = defineSetting<boolean>('chat.executePrompt.enabled', false);

export const CompletionsFetcher = defineExpSetting<FetcherId | undefined>('chat.completionsFetcher', undefined);
export const NextEditSuggestionsFetcher = defineExpSetting<FetcherId | undefined>('chat.nesFetcher', undefined);
}

export function getAllConfigKeys(): string[] {
Expand Down
34 changes: 27 additions & 7 deletions src/platform/networking/node/fetcherFallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,28 @@

import { Readable } from 'stream';
import { ILogService } from '../../log/common/logService';
import { FetchOptions, Response } from '../common/fetcherService';
import { FetcherId, FetchOptions, Response } from '../common/fetcherService';
import { IFetcher } from '../common/networking';
import { Config, ConfigKey, IConfigurationService } from '../../configuration/common/configurationService';


export async function fetchWithFallbacks(availableFetchers: readonly IFetcher[], url: string, options: FetchOptions, logService: ILogService): Promise<{ response: Response; updatedFetchers?: IFetcher[] }> {
const fetcherConfigKeys: Record<FetcherId, Config<boolean>> = {
'electron-fetch': ConfigKey.Shared.DebugUseElectronFetcher,
'node-fetch': ConfigKey.Shared.DebugUseNodeFetchFetcher,
'node-http': ConfigKey.Shared.DebugUseNodeFetcher,
};

export async function fetchWithFallbacks(availableFetchers: readonly IFetcher[], url: string, options: FetchOptions, knownBadFetchers: Set<string>, configurationService: IConfigurationService, logService: ILogService): Promise<{ response: Response; updatedFetchers?: IFetcher[]; updatedKnownBadFetchers?: Set<string> }> {
if (options.retryFallbacks && availableFetchers.length > 1) {
let firstResult: { ok: boolean; response: Response } | { ok: false; err: any } | undefined;
const updatedKnownBadFetchers = new Set<string>();
for (const fetcher of availableFetchers) {
const result = await tryFetch(fetcher, url, options, logService);
if (fetcher === availableFetchers[0]) {
firstResult = result;
}
if (!result.ok) {
updatedKnownBadFetchers.add(fetcher.getUserAgentLibrary());
continue;
}
if (fetcher !== availableFetchers[0]) {
Expand All @@ -29,7 +38,7 @@ export async function fetchWithFallbacks(availableFetchers: readonly IFetcher[],
const updatedFetchers = availableFetchers.slice();
updatedFetchers.splice(updatedFetchers.indexOf(fetcher), 1);
updatedFetchers.unshift(fetcher);
return { response: result.response, updatedFetchers };
return { response: result.response, updatedFetchers, updatedKnownBadFetchers };
}
return { response: result.response };
}
Expand All @@ -38,12 +47,23 @@ export async function fetchWithFallbacks(availableFetchers: readonly IFetcher[],
}
throw firstResult!.err;
}
const fetcher = options.useFetcher && availableFetchers.find(f => f.getUserAgentLibrary() === options.useFetcher) || availableFetchers[0];
let fetcher = availableFetchers[0];
if (options.useFetcher) {
if (options.useFetcher === fetcher.getUserAgentLibrary()) {
logService.debug(`FetcherService: using ${options.useFetcher} as requested.`);
if (knownBadFetchers.has(options.useFetcher)) {
logService.trace(`FetcherService: not using requested fetcher ${options.useFetcher} as it is known to be failing, using ${fetcher.getUserAgentLibrary()} instead.`);
} else {
logService.info(`FetcherService: could not find requested fetcher ${options.useFetcher}, using ${fetcher.getUserAgentLibrary()} instead.`);
const configKey = fetcherConfigKeys[options.useFetcher];
if (configKey && configurationService.inspectConfig(configKey)?.globalValue === false) {
logService.trace(`FetcherService: not using requested fetcher ${options.useFetcher} as it is disabled in user settings, using ${fetcher.getUserAgentLibrary()} instead.`);
} else {
const requestedFetcher = availableFetchers.find(f => f.getUserAgentLibrary() === options.useFetcher);
if (requestedFetcher) {
fetcher = requestedFetcher;
logService.trace(`FetcherService: using ${options.useFetcher} as requested.`);
} else {
logService.info(`FetcherService: could not find requested fetcher ${options.useFetcher}, using ${fetcher.getUserAgentLibrary()} instead.`);
}
}
}
}
return { response: await fetcher.fetch(url, options) };
Expand Down
70 changes: 65 additions & 5 deletions src/platform/networking/test/node/fetcherFallback.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@ import { Readable } from 'stream';
import { suite, test } from 'vitest';
import { FakeHeaders } from '../../../test/node/fetcher';
import { TestLogService } from '../../../testing/common/testLogService';
import { FetchOptions, Response } from '../../common/fetcherService';
import { FetcherId, FetchOptions, Response } from '../../common/fetcherService';
import { IFetcher } from '../../common/networking';
import { fetchWithFallbacks } from '../../node/fetcherFallback';
import { DefaultsOnlyConfigurationService } from '../../../configuration/common/defaultsOnlyConfigurationService';
import { InMemoryConfigurationService } from '../../../configuration/test/common/inMemoryConfigurationService';
import { ConfigKey } from '../../../configuration/common/configurationService';

suite('FetcherFallback Test Suite', function () {

const knownBadFetchers = new Set<FetcherId>();
const logService = new TestLogService();
const configurationService = new DefaultsOnlyConfigurationService();
const someHTML = '<html>...</html>';
const someJSON = '{"key": "value"}';

Expand All @@ -24,9 +29,10 @@ suite('FetcherFallback Test Suite', function () {
{ name: 'fetcher2', response: createFakeResponse(200, someJSON) },
];
const testFetchers = createTestFetchers(fetcherSpec);
const { response, updatedFetchers } = await fetchWithFallbacks(testFetchers.fetchers, 'https://example.com', { expectJSON: true, retryFallbacks: true }, logService);
const { response, updatedFetchers, updatedKnownBadFetchers } = await fetchWithFallbacks(testFetchers.fetchers, 'https://example.com', { expectJSON: true, retryFallbacks: true }, knownBadFetchers, configurationService, logService);
assert.deepStrictEqual(testFetchers.calls.map(c => c.name), fetcherSpec.slice(0, 1).map(f => f.name)); // only first fetcher called
assert.strictEqual(updatedFetchers, undefined);
assert.strictEqual(updatedKnownBadFetchers, undefined);
assert.strictEqual(response.status, 200);
const json = await response.json();
assert.deepStrictEqual(json, JSON.parse(someJSON));
Expand All @@ -39,11 +45,14 @@ suite('FetcherFallback Test Suite', function () {
{ name: 'fetcher1', response: createFakeResponse(200, someHTML) },
];
const testFetchers = createTestFetchers(fetcherSpec);
const { response, updatedFetchers } = await fetchWithFallbacks(testFetchers.fetchers, 'https://example.com', { expectJSON: true, retryFallbacks: true }, logService);
const { response, updatedFetchers, updatedKnownBadFetchers } = await fetchWithFallbacks(testFetchers.fetchers, 'https://example.com', { expectJSON: true, retryFallbacks: true }, knownBadFetchers, configurationService, logService);
assert.deepStrictEqual(testFetchers.calls.map(c => c.name), fetcherSpec.map(f => f.name));
assert.ok(updatedFetchers);
assert.strictEqual(updatedFetchers[0], testFetchers.fetchers[1]);
assert.strictEqual(updatedFetchers[1], testFetchers.fetchers[0]);
assert.ok(updatedKnownBadFetchers);
assert.strictEqual(updatedKnownBadFetchers.size, 1);
assert.strictEqual(updatedKnownBadFetchers.has('fetcher1'), true);
assert.strictEqual(response.status, 200);
const json = await response.json();
assert.deepStrictEqual(json, JSON.parse(someJSON));
Expand All @@ -55,9 +64,10 @@ suite('FetcherFallback Test Suite', function () {
{ name: 'fetcher2', response: createFakeResponse(401, someJSON) },
];
const testFetchers = createTestFetchers(fetcherSpec);
const { response, updatedFetchers } = await fetchWithFallbacks(testFetchers.fetchers, 'https://example.com', { expectJSON: true, retryFallbacks: true }, logService);
const { response, updatedFetchers, updatedKnownBadFetchers } = await fetchWithFallbacks(testFetchers.fetchers, 'https://example.com', { expectJSON: true, retryFallbacks: true }, knownBadFetchers, configurationService, logService);
assert.deepStrictEqual(testFetchers.calls.map(c => c.name), fetcherSpec.map(f => f.name));
assert.strictEqual(updatedFetchers, undefined);
assert.strictEqual(updatedKnownBadFetchers, undefined);
assert.strictEqual(response.status, 407);
const text = await response.text();
assert.deepStrictEqual(text, someHTML);
Expand All @@ -70,14 +80,64 @@ suite('FetcherFallback Test Suite', function () {
];
const testFetchers = createTestFetchers(fetcherSpec);
try {
await fetchWithFallbacks(testFetchers.fetchers, 'https://example.com', { expectJSON: true, retryFallbacks: true }, logService);
await fetchWithFallbacks(testFetchers.fetchers, 'https://example.com', { expectJSON: true, retryFallbacks: true }, knownBadFetchers, configurationService, logService);
assert.fail('Expected to throw');
} catch (err) {
assert.ok(err instanceof Error);
assert.strictEqual(err.message, 'fetcher1 error');
assert.deepStrictEqual(testFetchers.calls.map(c => c.name), fetcherSpec.map(f => f.name));
}
});

test('useFetcher option selects second fetcher', async function () {
const fetcherSpec = [
{ name: 'electron-fetch', response: createFakeResponse(200, someJSON) },
{ name: 'node-fetch', response: createFakeResponse(200, someJSON) },
];
const testFetchers = createTestFetchers(fetcherSpec);
const { response, updatedFetchers, updatedKnownBadFetchers } = await fetchWithFallbacks(testFetchers.fetchers, 'https://example.com', { useFetcher: 'node-fetch' }, knownBadFetchers, configurationService, logService);
assert.deepStrictEqual(testFetchers.calls.map(c => c.name), ['node-fetch']); // only second fetcher called
assert.strictEqual(updatedFetchers, undefined);
assert.strictEqual(updatedKnownBadFetchers, undefined);
assert.strictEqual(response.status, 200);
const json = await response.json();
assert.deepStrictEqual(json, JSON.parse(someJSON));
});

test('useFetcher option falls back to first fetcher when requested fetcher is disabled', async function () {
const fetcherSpec = [
{ name: 'electron-fetch', response: createFakeResponse(200, someJSON) },
{ name: 'node-fetch', response: createFakeResponse(200, someJSON) },
];
const testFetchers = createTestFetchers(fetcherSpec);
const configServiceWithDisabledNodeFetch = new InMemoryConfigurationService(
configurationService,
new Map([[ConfigKey.Shared.DebugUseNodeFetchFetcher, false]])
);
const { response, updatedFetchers, updatedKnownBadFetchers } = await fetchWithFallbacks(testFetchers.fetchers, 'https://example.com', { useFetcher: 'node-fetch' }, knownBadFetchers, configServiceWithDisabledNodeFetch, logService);
assert.deepStrictEqual(testFetchers.calls.map(c => c.name), ['electron-fetch']); // first fetcher used instead
assert.strictEqual(updatedFetchers, undefined);
assert.strictEqual(updatedKnownBadFetchers, undefined);
assert.strictEqual(response.status, 200);
const json = await response.json();
assert.deepStrictEqual(json, JSON.parse(someJSON));
});

test('useFetcher option falls back to first fetcher when requested fetcher is known bad', async function () {
const fetcherSpec = [
{ name: 'electron-fetch', response: createFakeResponse(200, someJSON) },
{ name: 'node-fetch', response: createFakeResponse(200, someJSON) },
];
const testFetchers = createTestFetchers(fetcherSpec);
const knownBadFetchersWithNodeFetch = new Set<FetcherId>(['node-fetch']);
const { response, updatedFetchers, updatedKnownBadFetchers } = await fetchWithFallbacks(testFetchers.fetchers, 'https://example.com', { useFetcher: 'node-fetch' }, knownBadFetchersWithNodeFetch, configurationService, logService);
assert.deepStrictEqual(testFetchers.calls.map(c => c.name), ['electron-fetch']); // first fetcher used instead
assert.strictEqual(updatedFetchers, undefined);
assert.strictEqual(updatedKnownBadFetchers, undefined);
assert.strictEqual(response.status, 200);
const json = await response.json();
assert.deepStrictEqual(json, JSON.parse(someJSON));
});
});

function createTestFetchers(fetcherSpecs: Array<{ name: string; response: Response | Error }>) {
Expand Down
6 changes: 5 additions & 1 deletion src/platform/networking/vscode-node/fetcherServiceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class FetcherService implements IFetcherService {

declare readonly _serviceBrand: undefined;
private _availableFetchers: readonly IFetcher[] | undefined;
private _knownBadFetchers = new Set<string>();
private _experimentationService: IExperimentationService | undefined;

constructor(
Expand Down Expand Up @@ -88,10 +89,13 @@ export class FetcherService implements IFetcherService {
}

async fetch(url: string, options: FetchOptions): Promise<Response> {
const { response: res, updatedFetchers } = await fetchWithFallbacks(this._getAvailableFetchers(), url, options, this._logService);
const { response: res, updatedFetchers, updatedKnownBadFetchers } = await fetchWithFallbacks(this._getAvailableFetchers(), url, options, this._knownBadFetchers, this._configurationService, this._logService);
if (updatedFetchers) {
this._availableFetchers = updatedFetchers;
}
if (updatedKnownBadFetchers) {
this._knownBadFetchers = updatedKnownBadFetchers;
}
return res;
}

Expand Down