Skip to content

Commit 5c1d517

Browse files
authored
Fix: Exp enable completions and NES fetcher (#1245)
1 parent 8f71557 commit 5c1d517

File tree

7 files changed

+132
-15
lines changed

7 files changed

+132
-15
lines changed

package.json

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"version": "0.33.0",
66
"build": "1",
77
"internalAIKey": "1058ec22-3c95-4951-8443-f26c1f325911",
8-
"completionsCore": "74943bef72acfd94931c6ec2c1c7420c8c932336",
8+
"completionsCore": "07be33f7faf935076909fc82bc0f5ac578cca983",
99
"completionsCoreVersion": "1.372.0",
1010
"internalLargeStorageAriaKey": "ec712b3202c5462fb6877acae7f1f9d7-c19ad55e-3e3c-4f99-984b-827f6d95bd9e-6917",
1111
"ariaKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255",
@@ -3128,6 +3128,30 @@
31283128
"tags": [
31293129
"experimental"
31303130
]
3131+
},
3132+
"github.copilot.chat.completionsFetcher": {
3133+
"type": ["string", "null"],
3134+
"markdownDescription": "%github.copilot.config.completionsFetcher%",
3135+
"tags": [
3136+
"experimental",
3137+
"onExp"
3138+
],
3139+
"enum": [
3140+
"electron-fetch",
3141+
"node-fetch"
3142+
]
3143+
},
3144+
"github.copilot.chat.nesFetcher": {
3145+
"type": ["string", "null"],
3146+
"markdownDescription": "%github.copilot.config.nesFetcher%",
3147+
"tags": [
3148+
"experimental",
3149+
"onExp"
3150+
],
3151+
"enum": [
3152+
"electron-fetch",
3153+
"node-fetch"
3154+
]
31313155
}
31323156
}
31333157
}

package.nls.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,5 +305,7 @@
305305
"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.",
306306
"github.copilot.config.responsesApiReasoningEffort": "Sets the reasoning effort used for the Responses API. Requires `#github.copilot.chat.useResponsesApi#`.",
307307
"github.copilot.config.responsesApiReasoningSummary": "Sets the reasoning summary style used for the Responses API. Requires `#github.copilot.chat.useResponsesApi#`.",
308-
"github.copilot.config.executePrompt.enabled": "The executePrompt tool enables the agent to execute tasks in a separate, isolated context."
308+
"github.copilot.config.executePrompt.enabled": "The executePrompt tool enables the agent to execute tasks in a separate, isolated context.",
309+
"github.copilot.config.completionsFetcher": "Sets the fetcher used for the inline completions.",
310+
"github.copilot.config.nesFetcher": "Sets the fetcher used for the next edit suggestions."
309311
}

src/extension/xtab/node/xtabProvider.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,8 @@ export class XtabProvider implements IStatelessNextEditProvider {
547547
) {
548548
const tracer = parentTracer.sub('streamEdits');
549549

550+
const useFetcher = this.configService.getExperimentBasedConfig(ConfigKey.NextEditSuggestionsFetcher, this.expService) || undefined;
551+
550552
const fetchStreamSource = new FetchStreamSource();
551553

552554
const fetchRequestStopWatch = new StopWatch();
@@ -592,6 +594,7 @@ export class XtabProvider implements IStatelessNextEditProvider {
592594
telemetryProperties: {
593595
requestId: request.id,
594596
},
597+
useFetcher,
595598
},
596599
cancellationToken,
597600
);

src/platform/configuration/common/configurationService.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { ResponseProcessor } from '../../inlineEdits/common/responseProcessor';
2020
import { AlternativeNotebookFormat } from '../../notebook/common/alternativeContentFormat';
2121
import { IExperimentationService } from '../../telemetry/common/nullExperimentationService';
2222
import { IValidator, vBoolean, vString } from './validator';
23+
import { FetcherId } from '../../networking/common/fetcherService';
2324

2425
export const CopilotConfigPrefix = 'github.copilot';
2526

@@ -811,6 +812,9 @@ export namespace ConfigKey {
811812
export const GrokCodeAlternatePrompt = defineExpSetting<string>('chat.grokCodeAlternatePrompt', 'default');
812813
export const ClaudeSonnet45AlternatePrompt = defineExpSetting<string>('chat.claudeSonnet45AlternatePrompt', 'default');
813814
export const ExecutePromptEnabled = defineSetting<boolean>('chat.executePrompt.enabled', false);
815+
816+
export const CompletionsFetcher = defineExpSetting<FetcherId | undefined>('chat.completionsFetcher', undefined);
817+
export const NextEditSuggestionsFetcher = defineExpSetting<FetcherId | undefined>('chat.nesFetcher', undefined);
814818
}
815819

816820
export function getAllConfigKeys(): string[] {

src/platform/networking/node/fetcherFallback.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,28 @@
55

66
import { Readable } from 'stream';
77
import { ILogService } from '../../log/common/logService';
8-
import { FetchOptions, Response } from '../common/fetcherService';
8+
import { FetcherId, FetchOptions, Response } from '../common/fetcherService';
99
import { IFetcher } from '../common/networking';
10+
import { Config, ConfigKey, IConfigurationService } from '../../configuration/common/configurationService';
1011

1112

12-
export async function fetchWithFallbacks(availableFetchers: readonly IFetcher[], url: string, options: FetchOptions, logService: ILogService): Promise<{ response: Response; updatedFetchers?: IFetcher[] }> {
13+
const fetcherConfigKeys: Record<FetcherId, Config<boolean>> = {
14+
'electron-fetch': ConfigKey.Shared.DebugUseElectronFetcher,
15+
'node-fetch': ConfigKey.Shared.DebugUseNodeFetchFetcher,
16+
'node-http': ConfigKey.Shared.DebugUseNodeFetcher,
17+
};
18+
19+
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> }> {
1320
if (options.retryFallbacks && availableFetchers.length > 1) {
1421
let firstResult: { ok: boolean; response: Response } | { ok: false; err: any } | undefined;
22+
const updatedKnownBadFetchers = new Set<string>();
1523
for (const fetcher of availableFetchers) {
1624
const result = await tryFetch(fetcher, url, options, logService);
1725
if (fetcher === availableFetchers[0]) {
1826
firstResult = result;
1927
}
2028
if (!result.ok) {
29+
updatedKnownBadFetchers.add(fetcher.getUserAgentLibrary());
2130
continue;
2231
}
2332
if (fetcher !== availableFetchers[0]) {
@@ -29,7 +38,7 @@ export async function fetchWithFallbacks(availableFetchers: readonly IFetcher[],
2938
const updatedFetchers = availableFetchers.slice();
3039
updatedFetchers.splice(updatedFetchers.indexOf(fetcher), 1);
3140
updatedFetchers.unshift(fetcher);
32-
return { response: result.response, updatedFetchers };
41+
return { response: result.response, updatedFetchers, updatedKnownBadFetchers };
3342
}
3443
return { response: result.response };
3544
}
@@ -38,12 +47,23 @@ export async function fetchWithFallbacks(availableFetchers: readonly IFetcher[],
3847
}
3948
throw firstResult!.err;
4049
}
41-
const fetcher = options.useFetcher && availableFetchers.find(f => f.getUserAgentLibrary() === options.useFetcher) || availableFetchers[0];
50+
let fetcher = availableFetchers[0];
4251
if (options.useFetcher) {
43-
if (options.useFetcher === fetcher.getUserAgentLibrary()) {
44-
logService.debug(`FetcherService: using ${options.useFetcher} as requested.`);
52+
if (knownBadFetchers.has(options.useFetcher)) {
53+
logService.trace(`FetcherService: not using requested fetcher ${options.useFetcher} as it is known to be failing, using ${fetcher.getUserAgentLibrary()} instead.`);
4554
} else {
46-
logService.info(`FetcherService: could not find requested fetcher ${options.useFetcher}, using ${fetcher.getUserAgentLibrary()} instead.`);
55+
const configKey = fetcherConfigKeys[options.useFetcher];
56+
if (configKey && configurationService.inspectConfig(configKey)?.globalValue === false) {
57+
logService.trace(`FetcherService: not using requested fetcher ${options.useFetcher} as it is disabled in user settings, using ${fetcher.getUserAgentLibrary()} instead.`);
58+
} else {
59+
const requestedFetcher = availableFetchers.find(f => f.getUserAgentLibrary() === options.useFetcher);
60+
if (requestedFetcher) {
61+
fetcher = requestedFetcher;
62+
logService.trace(`FetcherService: using ${options.useFetcher} as requested.`);
63+
} else {
64+
logService.info(`FetcherService: could not find requested fetcher ${options.useFetcher}, using ${fetcher.getUserAgentLibrary()} instead.`);
65+
}
66+
}
4767
}
4868
}
4969
return { response: await fetcher.fetch(url, options) };

src/platform/networking/test/node/fetcherFallback.spec.ts

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@ import { Readable } from 'stream';
88
import { suite, test } from 'vitest';
99
import { FakeHeaders } from '../../../test/node/fetcher';
1010
import { TestLogService } from '../../../testing/common/testLogService';
11-
import { FetchOptions, Response } from '../../common/fetcherService';
11+
import { FetcherId, FetchOptions, Response } from '../../common/fetcherService';
1212
import { IFetcher } from '../../common/networking';
1313
import { fetchWithFallbacks } from '../../node/fetcherFallback';
14+
import { DefaultsOnlyConfigurationService } from '../../../configuration/common/defaultsOnlyConfigurationService';
15+
import { InMemoryConfigurationService } from '../../../configuration/test/common/inMemoryConfigurationService';
16+
import { ConfigKey } from '../../../configuration/common/configurationService';
1417

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

20+
const knownBadFetchers = new Set<FetcherId>();
1721
const logService = new TestLogService();
22+
const configurationService = new DefaultsOnlyConfigurationService();
1823
const someHTML = '<html>...</html>';
1924
const someJSON = '{"key": "value"}';
2025

@@ -24,9 +29,10 @@ suite('FetcherFallback Test Suite', function () {
2429
{ name: 'fetcher2', response: createFakeResponse(200, someJSON) },
2530
];
2631
const testFetchers = createTestFetchers(fetcherSpec);
27-
const { response, updatedFetchers } = await fetchWithFallbacks(testFetchers.fetchers, 'https://example.com', { expectJSON: true, retryFallbacks: true }, logService);
32+
const { response, updatedFetchers, updatedKnownBadFetchers } = await fetchWithFallbacks(testFetchers.fetchers, 'https://example.com', { expectJSON: true, retryFallbacks: true }, knownBadFetchers, configurationService, logService);
2833
assert.deepStrictEqual(testFetchers.calls.map(c => c.name), fetcherSpec.slice(0, 1).map(f => f.name)); // only first fetcher called
2934
assert.strictEqual(updatedFetchers, undefined);
35+
assert.strictEqual(updatedKnownBadFetchers, undefined);
3036
assert.strictEqual(response.status, 200);
3137
const json = await response.json();
3238
assert.deepStrictEqual(json, JSON.parse(someJSON));
@@ -39,11 +45,14 @@ suite('FetcherFallback Test Suite', function () {
3945
{ name: 'fetcher1', response: createFakeResponse(200, someHTML) },
4046
];
4147
const testFetchers = createTestFetchers(fetcherSpec);
42-
const { response, updatedFetchers } = await fetchWithFallbacks(testFetchers.fetchers, 'https://example.com', { expectJSON: true, retryFallbacks: true }, logService);
48+
const { response, updatedFetchers, updatedKnownBadFetchers } = await fetchWithFallbacks(testFetchers.fetchers, 'https://example.com', { expectJSON: true, retryFallbacks: true }, knownBadFetchers, configurationService, logService);
4349
assert.deepStrictEqual(testFetchers.calls.map(c => c.name), fetcherSpec.map(f => f.name));
4450
assert.ok(updatedFetchers);
4551
assert.strictEqual(updatedFetchers[0], testFetchers.fetchers[1]);
4652
assert.strictEqual(updatedFetchers[1], testFetchers.fetchers[0]);
53+
assert.ok(updatedKnownBadFetchers);
54+
assert.strictEqual(updatedKnownBadFetchers.size, 1);
55+
assert.strictEqual(updatedKnownBadFetchers.has('fetcher1'), true);
4756
assert.strictEqual(response.status, 200);
4857
const json = await response.json();
4958
assert.deepStrictEqual(json, JSON.parse(someJSON));
@@ -55,9 +64,10 @@ suite('FetcherFallback Test Suite', function () {
5564
{ name: 'fetcher2', response: createFakeResponse(401, someJSON) },
5665
];
5766
const testFetchers = createTestFetchers(fetcherSpec);
58-
const { response, updatedFetchers } = await fetchWithFallbacks(testFetchers.fetchers, 'https://example.com', { expectJSON: true, retryFallbacks: true }, logService);
67+
const { response, updatedFetchers, updatedKnownBadFetchers } = await fetchWithFallbacks(testFetchers.fetchers, 'https://example.com', { expectJSON: true, retryFallbacks: true }, knownBadFetchers, configurationService, logService);
5968
assert.deepStrictEqual(testFetchers.calls.map(c => c.name), fetcherSpec.map(f => f.name));
6069
assert.strictEqual(updatedFetchers, undefined);
70+
assert.strictEqual(updatedKnownBadFetchers, undefined);
6171
assert.strictEqual(response.status, 407);
6272
const text = await response.text();
6373
assert.deepStrictEqual(text, someHTML);
@@ -70,14 +80,64 @@ suite('FetcherFallback Test Suite', function () {
7080
];
7181
const testFetchers = createTestFetchers(fetcherSpec);
7282
try {
73-
await fetchWithFallbacks(testFetchers.fetchers, 'https://example.com', { expectJSON: true, retryFallbacks: true }, logService);
83+
await fetchWithFallbacks(testFetchers.fetchers, 'https://example.com', { expectJSON: true, retryFallbacks: true }, knownBadFetchers, configurationService, logService);
7484
assert.fail('Expected to throw');
7585
} catch (err) {
7686
assert.ok(err instanceof Error);
7787
assert.strictEqual(err.message, 'fetcher1 error');
7888
assert.deepStrictEqual(testFetchers.calls.map(c => c.name), fetcherSpec.map(f => f.name));
7989
}
8090
});
91+
92+
test('useFetcher option selects second fetcher', async function () {
93+
const fetcherSpec = [
94+
{ name: 'electron-fetch', response: createFakeResponse(200, someJSON) },
95+
{ name: 'node-fetch', response: createFakeResponse(200, someJSON) },
96+
];
97+
const testFetchers = createTestFetchers(fetcherSpec);
98+
const { response, updatedFetchers, updatedKnownBadFetchers } = await fetchWithFallbacks(testFetchers.fetchers, 'https://example.com', { useFetcher: 'node-fetch' }, knownBadFetchers, configurationService, logService);
99+
assert.deepStrictEqual(testFetchers.calls.map(c => c.name), ['node-fetch']); // only second fetcher called
100+
assert.strictEqual(updatedFetchers, undefined);
101+
assert.strictEqual(updatedKnownBadFetchers, undefined);
102+
assert.strictEqual(response.status, 200);
103+
const json = await response.json();
104+
assert.deepStrictEqual(json, JSON.parse(someJSON));
105+
});
106+
107+
test('useFetcher option falls back to first fetcher when requested fetcher is disabled', async function () {
108+
const fetcherSpec = [
109+
{ name: 'electron-fetch', response: createFakeResponse(200, someJSON) },
110+
{ name: 'node-fetch', response: createFakeResponse(200, someJSON) },
111+
];
112+
const testFetchers = createTestFetchers(fetcherSpec);
113+
const configServiceWithDisabledNodeFetch = new InMemoryConfigurationService(
114+
configurationService,
115+
new Map([[ConfigKey.Shared.DebugUseNodeFetchFetcher, false]])
116+
);
117+
const { response, updatedFetchers, updatedKnownBadFetchers } = await fetchWithFallbacks(testFetchers.fetchers, 'https://example.com', { useFetcher: 'node-fetch' }, knownBadFetchers, configServiceWithDisabledNodeFetch, logService);
118+
assert.deepStrictEqual(testFetchers.calls.map(c => c.name), ['electron-fetch']); // first fetcher used instead
119+
assert.strictEqual(updatedFetchers, undefined);
120+
assert.strictEqual(updatedKnownBadFetchers, undefined);
121+
assert.strictEqual(response.status, 200);
122+
const json = await response.json();
123+
assert.deepStrictEqual(json, JSON.parse(someJSON));
124+
});
125+
126+
test('useFetcher option falls back to first fetcher when requested fetcher is known bad', async function () {
127+
const fetcherSpec = [
128+
{ name: 'electron-fetch', response: createFakeResponse(200, someJSON) },
129+
{ name: 'node-fetch', response: createFakeResponse(200, someJSON) },
130+
];
131+
const testFetchers = createTestFetchers(fetcherSpec);
132+
const knownBadFetchersWithNodeFetch = new Set<FetcherId>(['node-fetch']);
133+
const { response, updatedFetchers, updatedKnownBadFetchers } = await fetchWithFallbacks(testFetchers.fetchers, 'https://example.com', { useFetcher: 'node-fetch' }, knownBadFetchersWithNodeFetch, configurationService, logService);
134+
assert.deepStrictEqual(testFetchers.calls.map(c => c.name), ['electron-fetch']); // first fetcher used instead
135+
assert.strictEqual(updatedFetchers, undefined);
136+
assert.strictEqual(updatedKnownBadFetchers, undefined);
137+
assert.strictEqual(response.status, 200);
138+
const json = await response.json();
139+
assert.deepStrictEqual(json, JSON.parse(someJSON));
140+
});
81141
});
82142

83143
function createTestFetchers(fetcherSpecs: Array<{ name: string; response: Response | Error }>) {

src/platform/networking/vscode-node/fetcherServiceImpl.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export class FetcherService implements IFetcherService {
1818

1919
declare readonly _serviceBrand: undefined;
2020
private _availableFetchers: readonly IFetcher[] | undefined;
21+
private _knownBadFetchers = new Set<string>();
2122
private _experimentationService: IExperimentationService | undefined;
2223

2324
constructor(
@@ -88,10 +89,13 @@ export class FetcherService implements IFetcherService {
8889
}
8990

9091
async fetch(url: string, options: FetchOptions): Promise<Response> {
91-
const { response: res, updatedFetchers } = await fetchWithFallbacks(this._getAvailableFetchers(), url, options, this._logService);
92+
const { response: res, updatedFetchers, updatedKnownBadFetchers } = await fetchWithFallbacks(this._getAvailableFetchers(), url, options, this._knownBadFetchers, this._configurationService, this._logService);
9293
if (updatedFetchers) {
9394
this._availableFetchers = updatedFetchers;
9495
}
96+
if (updatedKnownBadFetchers) {
97+
this._knownBadFetchers = updatedKnownBadFetchers;
98+
}
9599
return res;
96100
}
97101

0 commit comments

Comments
 (0)