diff --git a/package.json b/package.json index f0a2e5e9b..3314aeb80 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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" + ] } } } diff --git a/package.nls.json b/package.nls.json index add85b9fb..32f202d4c 100644 --- a/package.nls.json +++ b/package.nls.json @@ -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." } diff --git a/src/extension/xtab/node/xtabProvider.ts b/src/extension/xtab/node/xtabProvider.ts index 0ce557203..d95c3a843 100644 --- a/src/extension/xtab/node/xtabProvider.ts +++ b/src/extension/xtab/node/xtabProvider.ts @@ -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(); @@ -531,6 +533,7 @@ export class XtabProvider implements IStatelessNextEditProvider { telemetryProperties: { requestId: request.id, }, + useFetcher, }, cancellationToken, ); diff --git a/src/platform/configuration/common/configurationService.ts b/src/platform/configuration/common/configurationService.ts index 9a9d165bb..e90913fa3 100644 --- a/src/platform/configuration/common/configurationService.ts +++ b/src/platform/configuration/common/configurationService.ts @@ -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'; @@ -811,6 +812,9 @@ export namespace ConfigKey { export const GrokCodeAlternatePrompt = defineExpSetting('chat.grokCodeAlternatePrompt', 'default'); export const ClaudeSonnet45AlternatePrompt = defineExpSetting('chat.claudeSonnet45AlternatePrompt', 'default'); export const ExecutePromptEnabled = defineSetting('chat.executePrompt.enabled', false); + + export const CompletionsFetcher = defineExpSetting('chat.completionsFetcher', undefined); + export const NextEditSuggestionsFetcher = defineExpSetting('chat.nesFetcher', undefined); } export function getAllConfigKeys(): string[] { diff --git a/src/platform/networking/node/fetcherFallback.ts b/src/platform/networking/node/fetcherFallback.ts index 6b8093792..bf0c38241 100644 --- a/src/platform/networking/node/fetcherFallback.ts +++ b/src/platform/networking/node/fetcherFallback.ts @@ -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> = { + '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, configurationService: IConfigurationService, logService: ILogService): Promise<{ response: Response; updatedFetchers?: IFetcher[]; updatedKnownBadFetchers?: Set }> { if (options.retryFallbacks && availableFetchers.length > 1) { let firstResult: { ok: boolean; response: Response } | { ok: false; err: any } | undefined; + const updatedKnownBadFetchers = new Set(); 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]) { @@ -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 }; } @@ -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) }; diff --git a/src/platform/networking/test/node/fetcherFallback.spec.ts b/src/platform/networking/test/node/fetcherFallback.spec.ts index 80444f3f9..8c53ebd3d 100644 --- a/src/platform/networking/test/node/fetcherFallback.spec.ts +++ b/src/platform/networking/test/node/fetcherFallback.spec.ts @@ -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(); const logService = new TestLogService(); + const configurationService = new DefaultsOnlyConfigurationService(); const someHTML = '...'; const someJSON = '{"key": "value"}'; @@ -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)); @@ -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)); @@ -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); @@ -70,7 +80,7 @@ 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); @@ -78,6 +88,56 @@ suite('FetcherFallback Test Suite', function () { 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(['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 }>) { diff --git a/src/platform/networking/vscode-node/fetcherServiceImpl.ts b/src/platform/networking/vscode-node/fetcherServiceImpl.ts index 47cd1040c..aca9cb233 100644 --- a/src/platform/networking/vscode-node/fetcherServiceImpl.ts +++ b/src/platform/networking/vscode-node/fetcherServiceImpl.ts @@ -18,6 +18,7 @@ export class FetcherService implements IFetcherService { declare readonly _serviceBrand: undefined; private _availableFetchers: readonly IFetcher[] | undefined; + private _knownBadFetchers = new Set(); private _experimentationService: IExperimentationService | undefined; constructor( @@ -88,10 +89,13 @@ export class FetcherService implements IFetcherService { } async fetch(url: string, options: FetchOptions): Promise { - 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; }