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.33.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>();
Copy link
Preview

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updatedKnownBadFetchers is initialized empty instead of cloning/unioning the incoming knownBadFetchers, and the caller later replaces its state (see FetcherService) leading to loss of previously known bad fetchers; either (a) initialize with new Set(knownBadFetchers) and add new ones, or (b) return only newly failed fetchers but have the caller union rather than replace to ensure consistent accumulation semantics.

Suggested change
const updatedKnownBadFetchers = new Set<string>();
const updatedKnownBadFetchers = new Set(knownBadFetchers);

Copilot uses AI. Check for mistakes.

Comment on lines +19 to +22
Copy link
Preview

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter and return types use Set while the codebase introduces FetcherId; narrowing these to Set improves type safety and consistency with other parts (tests already use Set).

Suggested change
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>();
export async function fetchWithFallbacks(availableFetchers: readonly IFetcher[], url: string, options: FetchOptions, knownBadFetchers: Set<FetcherId>, configurationService: IConfigurationService, logService: ILogService): Promise<{ response: Response; updatedFetchers?: IFetcher[]; updatedKnownBadFetchers?: Set<FetcherId> }> {
if (options.retryFallbacks && availableFetchers.length > 1) {
let firstResult: { ok: boolean; response: Response } | { ok: false; err: any } | undefined;
const updatedKnownBadFetchers = new Set<FetcherId>();

Copilot uses AI. Check for mistakes.

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());
Copy link
Preview

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updatedKnownBadFetchers is initialized empty instead of cloning/unioning the incoming knownBadFetchers, and the caller later replaces its state (see FetcherService) leading to loss of previously known bad fetchers; either (a) initialize with new Set(knownBadFetchers) and add new ones, or (b) return only newly failed fetchers but have the caller union rather than replace to ensure consistent accumulation semantics.

Copilot uses AI. Check for mistakes.

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 };
Copy link
Preview

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updatedKnownBadFetchers is initialized empty instead of cloning/unioning the incoming knownBadFetchers, and the caller later replaces its state (see FetcherService) leading to loss of previously known bad fetchers; either (a) initialize with new Set(knownBadFetchers) and add new ones, or (b) return only newly failed fetchers but have the caller union rather than replace to ensure consistent accumulation semantics.

Copilot uses AI. Check for mistakes.

}
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;
Copy link
Preview

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replacing the existing this._knownBadFetchers set with updatedKnownBadFetchers discards previously known bad fetchers on calls where only new failures are surfaced; this causes earlier failing fetchers to be forgotten unless they fail again in the same run. Merge instead: for (const f of updatedKnownBadFetchers) this._knownBadFetchers.add(f); to preserve cumulative state.

Suggested change
this._knownBadFetchers = updatedKnownBadFetchers;
for (const f of updatedKnownBadFetchers) {
this._knownBadFetchers.add(f);
}

Copilot uses AI. Check for mistakes.

}
return res;
}

Expand Down