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
19 changes: 19 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4023,6 +4023,25 @@
"experimental"
]
},
"github.copilot.chat.customization.vscModelOverride": {
"type": [
"string",
"null"
],
"enum": [
null,
"A",
"B",
"C",
"D"
],
"default": null,
"markdownDescription": "%github.copilot.config.customization.vscModelOverride%",
"tags": [
"advanced",
"experimental"
]
},
"github.copilot.chat.edits.gemini3MultiReplaceString": {
"type": "boolean",
"default": false,
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@
"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.",
"github.copilot.config.debug.overrideChatEngine": "Override the chat model. This allows you to test with different models.\n\n**Note**: This is an advanced debugging setting and should not be used while self-hosting as it may lead to a different experience compared to end-users.",
"github.copilot.config.customization.vscModelOverride": "Override the hidden VSC model family classification used for prompt routing and capability checks. When set to `A`, `B`, `C`, or `D`, only that family returns `true` and the others return `false`. Set to `null` to use normal detection.",
"github.copilot.config.projectLabels.expanded": "Use the expanded format for project labels in prompts.",
"github.copilot.config.projectLabels.chat": "Add project labels in chat requests.",
"github.copilot.config.projectLabels.inline": "Add project labels in inline edit requests.",
Expand Down
10 changes: 9 additions & 1 deletion src/extension/prompt/node/chatMLFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,14 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
uiKind: ChatLocation.toString(location)
});

const modelOverride = this._configurationService.getConfig(ConfigKey.Advanced.CustomizationVSCModelOverride);
const customMetadata = modelOverride
? {
...opts.customMetadata,
vscModelOverrideFamily: `vscModel${modelOverride}`,
}
: opts.customMetadata;

const pendingLoggedChatRequest = this._requestLogger.logChatRequest(debugName, chatEndpoint, {
messages: opts.messages,
model: chatEndpoint.model,
Expand All @@ -188,7 +196,7 @@ export class ChatMLFetcherImpl extends AbstractChatMLFetcher {
body: requestBody,
ignoreStatefulMarker,
isConversationRequest: opts.isConversationRequest,
customMetadata: opts.customMetadata
customMetadata
});
let tokenCount = -1;
const streamRecorder = new FetchStreamRecorder(finishedCb);
Expand Down
21 changes: 19 additions & 2 deletions src/extension/prompts/node/agent/promptRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
*--------------------------------------------------------------------------------------------*/

import { BasePromptElementProps, PromptElement } from '@vscode/prompt-tsx';
import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';
import { VSCModelVariant } from '../../../../platform/endpoint/common/chatModelCapabilities';
import { ILogService } from '../../../../platform/log/common/logService';
import type { IChatEndpoint } from '../../../../platform/networking/common/networking';
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
import { CopilotIdentityRules } from '../base/copilotIdentity';
Expand Down Expand Up @@ -65,9 +68,18 @@ export const PromptRegistry = new class {
}

private async getPromptResolver(
endpoint: IChatEndpoint
endpoint: IChatEndpoint,
modelOverride?: VSCModelVariant | null
): Promise<IAgentPromptCtor | undefined> {

if (modelOverride) {
const overridePrefix = `vscModel${modelOverride}`;
const match = this.familyPrefixList.find(({ prefix }) => prefix === overridePrefix);
if (match) {
return match.prompt;
}
}

for (const prompt of this.promptsWithMatcher) {
const matches = await prompt.matchesModel(endpoint);
if (matches) {
Expand Down Expand Up @@ -95,9 +107,14 @@ export const PromptRegistry = new class {
instantiationService: IInstantiationService,
endpoint: IChatEndpoint,
): Promise<AgentPromptCustomizations> {
const promptResolverCtor = await this.getPromptResolver(endpoint);
const modelOverride = instantiationService.invokeFunction(accessor => accessor.get(IConfigurationService).getConfig(ConfigKey.Advanced.CustomizationVSCModelOverride));
const promptResolverCtor = await this.getPromptResolver(endpoint, modelOverride);
const agentPrompt = promptResolverCtor ? instantiationService.createInstance(promptResolverCtor) : undefined;

if (modelOverride) {
instantiationService.invokeFunction(accessor => accessor.get(ILogService).info(`[PromptRegistry] Using VSC model override '${modelOverride}' with resolver '${promptResolverCtor?.name ?? 'default'}' for endpoint family '${endpoint.family}'`));
}

return {
SystemPrompt: agentPrompt?.resolveSystemPrompt(endpoint) ?? DefaultAgentPrompt,
ReminderInstructionsClass: agentPrompt?.resolveReminderInstructions?.(endpoint) ?? DefaultReminderInstructions,
Expand Down
5 changes: 4 additions & 1 deletion src/platform/configuration/common/configurationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { ResponseProcessor } from '../../inlineEdits/common/responseProcessor';
import { FetcherId } from '../../networking/common/fetcherService';
import { AlternativeNotebookFormat } from '../../notebook/common/alternativeContentFormat';
import { IExperimentationService } from '../../telemetry/common/nullExperimentationService';
import { IValidator, vBoolean, vNumber, vString } from './validator';
import { IValidator, vBoolean, vEnum, vNullable, vNumber, vString } from './validator';

export const CopilotConfigPrefix = 'github.copilot';

Expand Down Expand Up @@ -688,6 +688,9 @@ export namespace ConfigKey {
/** Simulate GitHub authentication failures for testing. Can't be TeamInternal because we lose these flags as part of testing. */
export const DebugGitHubAuthFailWith = defineSetting<'NotAuthorized' | 'RequestFailed' | 'ParseFailed' | 'HTTP401' | 'RateLimited' | 'GitHubLoginFailed' | null>('chat.debug.githubAuthFailWith', ConfigType.Simple, null);

/** Override which VSC model variant is active. Only one can be true; when set, the others return false. */
export const CustomizationVSCModelOverride = defineSetting<'A' | 'B' | 'C' | 'D' | null>('chat.customization.vscModelOverride', ConfigType.Simple, null, vNullable(vEnum('A', 'B', 'C', 'D')));

// Agent debug logging settings — fileLogging.enabled is the canonical toggle
/** @deprecated Use ChatDebugFileLogging instead. Kept during experiment transition. */
export const AgentDebugLogEnabled = defineAndMigrateExpSetting<boolean>('agentDebugLog.enabled', 'chat.agentDebugLog.enabled', false);
Expand Down
21 changes: 17 additions & 4 deletions src/platform/endpoint/common/chatModelCapabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,21 @@ export function isGpt53Codex(model: LanguageModelChat | IChatEndpoint | string)
return family.startsWith('gpt-5.3-codex');
}

export function isVSCModelA(model: LanguageModelChat | IChatEndpoint) {
export type VSCModelVariant = 'A' | 'B' | 'C' | 'D';

export function isVSCModelA(model: LanguageModelChat | IChatEndpoint, override?: VSCModelVariant | null) {
if (override !== undefined && override !== null) {
return override === 'A';
}
Comment on lines +138 to +143
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

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

The VSC model override parameter is added to isVSCModel*(), but none of the capability helpers in this module pass an override value (they still call isVSCModelA/B/C/D with a single argument). As a result, the new config-driven override won’t actually affect capability checks unless the override is plumbed through at the call sites (or these helpers are updated to accept/pass an override).

Copilot uses AI. Check for mistakes.
const ID_hash = getCachedSha256Hash(getModelId(model));
const family_hash = getCachedSha256Hash(model.family);
return VSC_MODEL_HASHES_A.includes(ID_hash) || VSC_MODEL_HASHES_A.includes(family_hash);
}

export function isVSCModelB(model: LanguageModelChat | IChatEndpoint) {
export function isVSCModelB(model: LanguageModelChat | IChatEndpoint, override?: VSCModelVariant | null) {
if (override !== undefined && override !== null) {
return override === 'B';
}
const ID_hash = getCachedSha256Hash(getModelId(model));
const family_hash = getCachedSha256Hash(model.family);
return VSC_MODEL_HASHES_B.includes(ID_hash) || VSC_MODEL_HASHES_B.includes(family_hash);
Expand All @@ -154,13 +161,19 @@ export function isVSCModelReplaceStringSet(model: LanguageModelChat | IChatEndpo
return VSC_MODEL_HASHES_EDIT_TOOL_SET.includes(ID_hash) || VSC_MODEL_HASHES_EDIT_TOOL_SET.includes(family_hash);
}

export function isVSCModelC(model: LanguageModelChat | IChatEndpoint) {
export function isVSCModelC(model: LanguageModelChat | IChatEndpoint, override?: VSCModelVariant | null) {
if (override !== undefined && override !== null) {
return override === 'C';
}
const ID_hash = getCachedSha256Hash(getModelId(model));
const family_hash = getCachedSha256Hash(model.family);
return VSC_MODEL_HASHES_C.includes(ID_hash) || VSC_MODEL_HASHES_C.includes(family_hash);
}

export function isVSCModelD(model: LanguageModelChat | IChatEndpoint) {
export function isVSCModelD(model: LanguageModelChat | IChatEndpoint, override?: VSCModelVariant | null) {
if (override !== undefined && override !== null) {
return override === 'D';
}
const ID_hash = getCachedSha256Hash(getModelId(model));
const family_hash = getCachedSha256Hash(model.family);
return VSC_MODEL_HASHES_D.includes(ID_hash) || VSC_MODEL_HASHES_D.includes(family_hash);
Expand Down
22 changes: 21 additions & 1 deletion src/platform/endpoint/test/node/chatModelCapabilities.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { describe, expect, test } from 'vitest';
import type { IChatEndpoint } from '../../../networking/common/networking';
import { modelSupportsPDFDocuments } from '../../common/chatModelCapabilities';
import { isVSCModelA, isVSCModelB, isVSCModelC, isVSCModelD, modelSupportsPDFDocuments } from '../../common/chatModelCapabilities';

function fakeModel(family: string) {
return { family } as unknown as IChatEndpoint;
Expand All @@ -30,3 +30,23 @@ describe('modelSupportsPDFDocuments', () => {
expect(modelSupportsPDFDocuments(fakeModel('o4-mini'))).toBe(false);
});
});

describe('VSC model override', () => {
test('makes the selected family exclusively true', () => {
const model = { id: 'test-model', model: 'test-model', family: 'test-family' } as unknown as IChatEndpoint;

expect(isVSCModelA(model, 'C')).toBe(false);
expect(isVSCModelB(model, 'C')).toBe(false);
expect(isVSCModelC(model, 'C')).toBe(true);
expect(isVSCModelD(model, 'C')).toBe(false);
});

test('does not force a family when the override is unset', () => {
const model = { id: 'test-model', model: 'test-model', family: 'test-family' } as unknown as IChatEndpoint;

expect(isVSCModelA(model, null)).toBe(false);
expect(isVSCModelB(model, null)).toBe(false);
expect(isVSCModelC(model, null)).toBe(false);
expect(isVSCModelD(model, null)).toBe(false);
});
});
47 changes: 47 additions & 0 deletions src/platform/requestLogger/test/node/requestLogger.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { describe, expect, test } from 'vitest';
import { ChatLocation } from '../../../chat/common/commonTypes';
import type { IChatEndpoint } from '../../../networking/common/networking';
import { LoggedInfoKind, LoggedRequestKind, type ILoggedRequestInfo } from '../../node/requestLogger';
import { TestRequestLogger } from './testRequestLogger';

describe('RequestLogger custom metadata', () => {
test('preserves metadata values on logged requests', () => {
const logger = new TestRequestLogger();
const endpoint = {
model: 'test-model',
family: 'test-family',
} as IChatEndpoint;

const pending = logger.logChatRequest('debug-request', endpoint, {
messages: [],
ourRequestId: 'request-1',
model: 'test-model',
location: ChatLocation.Panel,
customMetadata: {
vscModelOverrideFamily: 'vscModelC',
extraFlag: true,
},
});

pending.resolveWithCancelation();

const requestInfo = logger.getRequests().find(entry => entry.kind === LoggedInfoKind.Request);
expect(requestInfo).toBeDefined();

const request = requestInfo as ILoggedRequestInfo;
expect(request.entry.type).toBe(LoggedRequestKind.ChatMLCancelation);
if (request.entry.type !== LoggedRequestKind.ChatMLCancelation) {
throw new Error('Expected a canceled request entry');
}

expect(request.entry.customMetadata).toEqual({
vscModelOverrideFamily: 'vscModelC',
extraFlag: true,
});
});
});