diff --git a/extensions/copilot/src/extension/inlineEdits/common/userInteractionMonitor.ts b/extensions/copilot/src/extension/inlineEdits/common/userInteractionMonitor.ts index 59ba85a1cc7e42..376343d1e4f934 100644 --- a/extensions/copilot/src/extension/inlineEdits/common/userInteractionMonitor.ts +++ b/extensions/copilot/src/extension/inlineEdits/common/userInteractionMonitor.ts @@ -5,6 +5,7 @@ import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; import { AggressivenessLevel, AggressivenessSetting, DEFAULT_USER_HAPPINESS_SCORE_CONFIGURATION, parseUserHappinessScoreConfigurationString, UserHappinessScoreConfiguration } from '../../../platform/inlineEdits/common/dataTypes/xtabPromptOptions'; +import { IInlineEditsModelService } from '../../../platform/inlineEdits/common/inlineEditsModelService'; import { ILogService } from '../../../platform/log/common/logService'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry'; @@ -174,6 +175,7 @@ export class UserInteractionMonitor { @IExperimentationService private readonly _experimentationService: IExperimentationService, @ILogService private readonly _logService: ILogService, @ITelemetryService private readonly _telemetryService: ITelemetryService, + @IInlineEditsModelService private readonly _modelService: IInlineEditsModelService, ) { } // Capture user interactions @@ -217,7 +219,7 @@ export class UserInteractionMonitor { // Creates a DelaySession based on recent user interactions public createDelaySession(requestTime: number | undefined): DelaySession { - const baseDebounceTime = this._configurationService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsDebounce, this._experimentationService); + const baseDebounceTime = this._modelService.selectedModelConfiguration().debounce ?? this._configurationService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsDebounce, this._experimentationService); const backoffDebounceEnabled = this._configurationService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsBackoffDebounceEnabled, this._experimentationService); const expectedTotalTime = backoffDebounceEnabled ? this._getExpectedTotalTime(baseDebounceTime) : undefined; diff --git a/extensions/copilot/src/extension/inlineEdits/node/nextEditProvider.ts b/extensions/copilot/src/extension/inlineEdits/node/nextEditProvider.ts index 73c7e94b542070..95abcc55289341 100644 --- a/extensions/copilot/src/extension/inlineEdits/node/nextEditProvider.ts +++ b/extensions/copilot/src/extension/inlineEdits/node/nextEditProvider.ts @@ -1136,8 +1136,9 @@ export class NextEditProvider extends Disposable implements INextEditProvider { experimentationService = new NullExperimentationService(); logService = new TestLogService(); telemetryService = new NullTelemetryService(); - monitor = new TestUserInteractionMonitor(configurationService, experimentationService, logService, telemetryService); + monitor = new TestUserInteractionMonitor(configurationService, experimentationService, logService, telemetryService, new NullInlineEditsModelService()); }); describe('history logging', () => { @@ -277,7 +278,7 @@ describe('UserInteractionMonitor', () => { const levelRejectionsRecent = monitor.getAggressivenessLevel().aggressivenessLevel; // Reset and do opposite order - monitor = new TestUserInteractionMonitor(configurationService, experimentationService, logService, telemetryService); + monitor = new TestUserInteractionMonitor(configurationService, experimentationService, logService, telemetryService, new NullInlineEditsModelService()); for (let i = 0; i < 5; i++) { monitor.handleRejection(); } @@ -356,7 +357,7 @@ describe('UserInteractionMonitor', () => { beforeEach(() => { configurationService.useAdaptiveAggressiveness(); mockTelemetryService = new MockTelemetryService(); - monitor = new TestUserInteractionMonitor(configurationService, experimentationService, logService, mockTelemetryService); + monitor = new TestUserInteractionMonitor(configurationService, experimentationService, logService, mockTelemetryService, new NullInlineEditsModelService()); }); test('emits telemetry event when config is invalid JSON', () => { diff --git a/extensions/copilot/src/extension/inlineEdits/vscode-node/inlineCompletionProvider.ts b/extensions/copilot/src/extension/inlineEdits/vscode-node/inlineCompletionProvider.ts index 6e8240d1f3ac90..4cf9fb29c9db1b 100644 --- a/extensions/copilot/src/extension/inlineEdits/vscode-node/inlineCompletionProvider.ts +++ b/extensions/copilot/src/extension/inlineEdits/vscode-node/inlineCompletionProvider.ts @@ -254,7 +254,8 @@ export class InlineCompletionProviderImpl extends Disposable implements InlineCo const isCompletionsEnabled = this._isCompletionsEnabled(document); - const unification = this._configurationService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsUnification, this._expService); + const unification = this._modelService.selectedModelConfiguration().supportsUnifiedCompletions + ?? this._configurationService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsUnification, this._expService); const isInlineEditsEnabled = this._configurationService.getExperimentBasedConfig(ConfigKey.InlineEditsEnabled, this._expService, { languageId: document.languageId }); @@ -467,7 +468,7 @@ export class InlineCompletionProviderImpl extends Disposable implements InlineCo // re-surface in any other form. Suppress here without evicting the cache entry — // when the cursor returns to an inline-renderable position, we'll serve it again. if ( - this._nesMimicGhostTextBehavior.get() + (this._modelService.selectedModelConfiguration().nesMimicGhostTextBehavior ?? this._nesMimicGhostTextBehavior.get()) && !isInlineCompletion && isLlmCompletionInfo(suggestionInfo) && suggestionInfo.suggestion.result?.cacheEntry?.wasRenderedAsInlineSuggestion diff --git a/extensions/copilot/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts b/extensions/copilot/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts index af512805a007d0..5d6b753a981d38 100644 --- a/extensions/copilot/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts +++ b/extensions/copilot/src/extension/inlineEdits/vscode-node/jointInlineCompletionProvider.ts @@ -12,6 +12,7 @@ import { IEnvService } from '../../../platform/env/common/envService'; import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext'; import { JointCompletionsProviderStrategy, JointCompletionsProviderTriggerChangeStrategy } from '../../../platform/inlineEdits/common/dataTypes/jointCompletionsProviderOptions'; import { InlineEditRequestLogContext } from '../../../platform/inlineEdits/common/inlineEditLogContext'; +import { IInlineEditsModelService } from '../../../platform/inlineEdits/common/inlineEditsModelService'; import { ObservableGit } from '../../../platform/inlineEdits/common/observableGit'; import { checkIfCursorAtEndOfLine, shortenOpportunityId } from '../../../platform/inlineEdits/common/utils/utils'; import { NesHistoryContextProvider } from '../../../platform/inlineEdits/common/workspaceEditTracker/nesHistoryContextProvider'; @@ -65,6 +66,7 @@ export class JointCompletionsProviderContribution extends Disposable implements // private readonly _yieldToCopilot = this._configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsYieldToCopilot, this._expService); private readonly _excludedProviders = this._configurationService.getExperimentBasedConfigObservable(ConfigKey.TeamInternal.InlineEditsExcludedProviders, this._expService).map(v => v ? v.split(',').map(v => v.trim()).filter(v => v !== '') : []); private readonly _copilotToken = observableFromEvent(this, this._authenticationService.onDidCopilotTokenChange, () => this._authenticationService.copilotToken); + private readonly _supportsUnifiedCompletions = observableFromEvent(this, this._modelService.onModelListUpdated, () => this._modelService.selectedModelConfiguration().supportsUnifiedCompletions ?? false); public readonly inlineEditsEnabled = derived(this, (reader) => { const copilotToken = this._copilotToken.read(reader); @@ -96,6 +98,7 @@ export class JointCompletionsProviderContribution extends Disposable implements @IExperimentationService private readonly _expService: IExperimentationService, @IAuthenticationService private readonly _authenticationService: IAuthenticationService, @IEnvService private readonly _envService: IEnvService, + @IInlineEditsModelService private readonly _modelService: IInlineEditsModelService, ) { super(); @@ -117,6 +120,11 @@ export class JointCompletionsProviderContribution extends Disposable implements reader.store.add(autorun((reader) => { const unificationStateValue = unificationState.read(reader); + // A model whose strategy bakes in `supportsUnifiedCompletions` runs as the single unified + // provider: this stands in for the `modelUnification` deployment toggle so the behavior can + // be driven purely from the selected model's prompting strategy. + const modelUnification = this._supportsUnifiedCompletions.read(reader) || (unificationStateValue?.modelUnification ?? false); + const excludes = this._excludedProviders.read(reader).slice(); let inlineEditProvider: InlineCompletionProviderImpl | undefined = undefined; @@ -211,7 +219,6 @@ export class JointCompletionsProviderContribution extends Disposable implements const isExcluded = excludes.includes(JointCompletionsProviderContribution.COMPLETIONS_GROUP_ID) && this.inlineEditsEnabled.read(reader); // @ulugbekna: note that we don't want it if modelUnification is on - const modelUnification = unificationStateValue?.modelUnification ?? false; if ( (!modelUnification || unificationStateValue?.codeUnification || extensionUnification || configEnabled || this._copilotToken.read(reader)?.isNoAuthUser) && !isExcluded @@ -229,7 +236,7 @@ export class JointCompletionsProviderContribution extends Disposable implements const singularProvider = reader.store.add(this._instantiationService.createInstance(JointCompletionsProvider, completionsProvider, inlineEditProvider)); - if (unificationStateValue?.modelUnification) { + if (modelUnification) { if (!excludes.includes('github.copilot')) { excludes.push('github.copilot'); } diff --git a/extensions/copilot/src/extension/xtab/common/promptCrafting.ts b/extensions/copilot/src/extension/xtab/common/promptCrafting.ts index 9af66e630ff703..90bbe81b760434 100644 --- a/extensions/copilot/src/extension/xtab/common/promptCrafting.ts +++ b/extensions/copilot/src/extension/xtab/common/promptCrafting.ts @@ -136,6 +136,7 @@ export function getUserPrompt(promptPieces: PromptPieces): UserPromptResult { break; case PromptingStrategy.PatchBased02: case PromptingStrategy.PatchBased02WithRecentLineNumbers: + case PromptingStrategy.PatchBased02Optimized: case PromptingStrategy.PatchBased02WithoutRecentLineNumbers: { const currentDocument = promptPieces.currentDocument; const cursorLine = currentDocument.lineWithCursor(); @@ -175,6 +176,7 @@ export function getUserPrompt(promptPieces: PromptPieces): UserPromptResult { opts.promptingStrategy !== PromptingStrategy.PatchBased01 && opts.promptingStrategy !== PromptingStrategy.PatchBased02 && opts.promptingStrategy !== PromptingStrategy.PatchBased02WithRecentLineNumbers && + opts.promptingStrategy !== PromptingStrategy.PatchBased02Optimized && opts.promptingStrategy !== PromptingStrategy.PatchBased02WithoutRecentLineNumbers; const packagedPrompt = includeBackticks ? wrapInBackticks(mainPrompt) : mainPrompt; @@ -379,6 +381,7 @@ function getPostScript(options: PromptOptions, currentFilePath: string, aggressi break; case PromptingStrategy.PatchBased02: case PromptingStrategy.PatchBased02WithRecentLineNumbers: + case PromptingStrategy.PatchBased02Optimized: case PromptingStrategy.PatchBased02WithoutRecentLineNumbers: postScript = eagernessPrompt === 'aggressionHighLow' ? aggressivenessLevel === AggressivenessLevel.Medium diff --git a/extensions/copilot/src/extension/xtab/node/xtabProvider.ts b/extensions/copilot/src/extension/xtab/node/xtabProvider.ts index 5a7e8095f2eadf..104f36f00c9c61 100644 --- a/extensions/copilot/src/extension/xtab/node/xtabProvider.ts +++ b/extensions/copilot/src/extension/xtab/node/xtabProvider.ts @@ -370,7 +370,7 @@ export class XtabProvider implements IStatelessNextEditProvider { const currentDocument = new CurrentDocument(activeDocument.documentAfterEdits, cursorPosition); - this._configureDebounceTimings(request, currentDocument, promptOptions, telemetry, delaySession, tracer); + this._configureDebounceTimings(request, currentDocument, promptOptions, modelServiceConfig, telemetry, delaySession, tracer); const areaAroundEditWindowLinesRange = computeAreaAroundEditWindowLinesRange(currentDocument); @@ -507,7 +507,7 @@ export class XtabProvider implements IStatelessNextEditProvider { const responseFormat = xtabPromptOptions.ResponseFormat.fromPromptingStrategy(promptOptions.promptingStrategy); - const prediction = this.getPredictedOutput(activeDocument, currentDocument.cursorLineOffset, editWindowLines, cursorLineInEditWindowOffset, responseFormat); + const prediction = this.getPredictedOutput(activeDocument, currentDocument.cursorLineOffset, editWindowLines, cursorLineInEditWindowOffset, responseFormat, modelServiceConfig); const systemMsg = pickSystemPrompt(promptOptions.promptingStrategy); const messages = constructMessages({ @@ -589,6 +589,7 @@ export class XtabProvider implements IStatelessNextEditProvider { request: StatelessNextEditRequest, currentDocument: CurrentDocument, promptOptions: ModelConfig, + modelServiceConfig: xtabPromptOptions.ModelConfiguration, telemetry: StatelessNextEditTelemetryBuilder, delaySession: DelaySession, tracer: ILogger, @@ -610,7 +611,7 @@ export class XtabProvider implements IStatelessNextEditProvider { delaySession.setExtraDebounce(inlineSuggestionDebounce); } else if (isCursorAtEndOfLine) { tracer.trace('Debouncing for cursor at end of line'); - delaySession.setExtraDebounce(this.configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsExtraDebounceEndOfLine, this.expService)); + delaySession.setExtraDebounce(modelServiceConfig.extraDebounceEndOfLine ?? this.configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsExtraDebounceEndOfLine, this.expService)); } else { tracer.trace('No extra debounce applied'); } @@ -1057,9 +1058,9 @@ export class XtabProvider implements IStatelessNextEditProvider { const lastLineLength = lastLine.length; const pseudoEditWindow = currentDocument.transformer.getOffsetRange(new Range(clippedTaggedCurrentDoc.keptRange.start + 1, 1, keptRangeEndExclusive, lastLineLength + 1)); const duplicateAdditionsMode = this.configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsXtabDuplicateAdditionsMode, this.expService); - const fastYieldLineWithCursor = this.configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsXtabProviderPatchFastYieldLineWithCursor, this.expService); + const fastYieldLineWithCursor = editStreamCtx.modelServiceConfig.patchFastYieldLineWithCursor ?? this.configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsXtabProviderPatchFastYieldLineWithCursor, this.expService); const fastYieldLineWithCursorMultiLine = this.configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsXtabProviderPatchFastYieldLineWithCursorMultiLine, this.expService); - const splitPatchOnDiff = this.configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsXtabSplitPatchOnDiff, this.expService); + const splitPatchOnDiff = editStreamCtx.modelServiceConfig.splitPatchOnDiff ?? this.configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsXtabSplitPatchOnDiff, this.expService); parseResult = new ResponseParseResult.DirectEdits( XtabPatchResponseHandler.handleResponse( linesStream, @@ -1636,14 +1637,14 @@ export class XtabProvider implements IStatelessNextEditProvider { return createProxyXtabEndpoint(this.instaService, configuredModelName); } - private getPredictedOutput(doc: StatelessNextEditDocument, cursorLineOffset: number, editWindowLines: string[], cursorLineInEditWindowOffset: number, responseFormat: xtabPromptOptions.ResponseFormat): Prediction | undefined { + private getPredictedOutput(doc: StatelessNextEditDocument, cursorLineOffset: number, editWindowLines: string[], cursorLineInEditWindowOffset: number, responseFormat: xtabPromptOptions.ResponseFormat, modelServiceConfig: xtabPromptOptions.ModelConfiguration): Prediction | undefined { const usePrediction = this.configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsXtabProviderUsePrediction, this.expService); if (!usePrediction) { return undefined; } // Only the CustomDiffPatch shape consults `patchModelPredictionKind`; skip the experiment lookup otherwise. const patchModelPredictionKind = responseFormat === xtabPromptOptions.ResponseFormat.CustomDiffPatch - ? this.configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsXtabProviderPatchModelPredictionKind, this.expService) + ? modelServiceConfig.patchModelPredictionKind ?? this.configService.getExperimentBasedConfig(ConfigKey.TeamInternal.InlineEditsXtabProviderPatchModelPredictionKind, this.expService) : xtabPromptOptions.PatchModelPrediction.FilePath; return { type: 'content', @@ -1819,6 +1820,7 @@ export function pickSystemPrompt(promptingStrategy: xtabPromptOptions.PromptingS case xtabPromptOptions.PromptingStrategy.PatchBased01: case xtabPromptOptions.PromptingStrategy.PatchBased02: case xtabPromptOptions.PromptingStrategy.PatchBased02WithRecentLineNumbers: + case xtabPromptOptions.PromptingStrategy.PatchBased02Optimized: case xtabPromptOptions.PromptingStrategy.PatchBased02WithoutRecentLineNumbers: case xtabPromptOptions.PromptingStrategy.Xtab275: case xtabPromptOptions.PromptingStrategy.XtabAggressiveness: diff --git a/extensions/copilot/src/extension/xtab/test/node/xtabProvider.spec.ts b/extensions/copilot/src/extension/xtab/test/node/xtabProvider.spec.ts index 572d61309d9bb9..c8de53e6aa0f49 100644 --- a/extensions/copilot/src/extension/xtab/test/node/xtabProvider.spec.ts +++ b/extensions/copilot/src/extension/xtab/test/node/xtabProvider.spec.ts @@ -167,6 +167,7 @@ describe('pickSystemPrompt', () => { PromptingStrategy.PatchBased01, PromptingStrategy.PatchBased02, PromptingStrategy.PatchBased02WithRecentLineNumbers, + PromptingStrategy.PatchBased02Optimized, PromptingStrategy.PatchBased02WithoutRecentLineNumbers, PromptingStrategy.Xtab275, PromptingStrategy.XtabAggressiveness, diff --git a/extensions/copilot/src/platform/inlineEdits/common/dataTypes/xtabPromptOptions.ts b/extensions/copilot/src/platform/inlineEdits/common/dataTypes/xtabPromptOptions.ts index ba571126085620..e56478d72a1840 100644 --- a/extensions/copilot/src/platform/inlineEdits/common/dataTypes/xtabPromptOptions.ts +++ b/extensions/copilot/src/platform/inlineEdits/common/dataTypes/xtabPromptOptions.ts @@ -542,6 +542,8 @@ export enum PromptingStrategy { PatchBased02 = 'patchBased02', /** PatchBased02 variant: line numbers on recent docs. */ PatchBased02WithRecentLineNumbers = 'patchBased02WithRecentLineNumbers', + /** Optimized PatchBased02 variant with line numbers on recent docs. */ + PatchBased02Optimized = 'patchBased02Optimized', /** PatchBased02 variant: no line numbers on recent docs. */ PatchBased02WithoutRecentLineNumbers = 'patchBased02WithoutRecentLineNumbers', /** @@ -608,6 +610,7 @@ export namespace ResponseFormat { case PromptingStrategy.PatchBased01: case PromptingStrategy.PatchBased02: case PromptingStrategy.PatchBased02WithRecentLineNumbers: + case PromptingStrategy.PatchBased02Optimized: case PromptingStrategy.PatchBased02WithoutRecentLineNumbers: return ResponseFormat.CustomDiffPatch; case PromptingStrategy.Xtab275EditIntent: @@ -683,6 +686,69 @@ export const LANGUAGE_CONTEXT_ENABLED_LANGUAGES: LanguageContextLanguages = { 'chatagent': true, }; +/** + * Shape of the predicted output we send to the patch-based model along with the prompt. + * In every example below, `{currentLineNumber}` is 0-based — matching `Patch.lineNumZeroBased` + * parsed by `XtabCustomDiffPatchResponseHandler`. + */ +export enum PatchModelPrediction { + /** + * Expects changes in the current file but doesn't expect where (line number is not specified). + * + * Example: + * + * ``` + * path/to/file: + * ``` + */ + FilePath = 'filePath', + /** + * Predicts the file path, cursor line number, and a deletion of the current line. + * The model is free to follow with further `-`/`+` lines as needed. + * + * Example: + * + * ``` + * path/to/file:{currentLineNumber} + * - class Foo { + * ``` + */ + CurrentLine = 'currentLine', + /** + * Expects the current line to be replaced. + * + * Example: + * + * ``` + * path/to/file:{currentLineNumber} + * - class Foo { + * + + * ``` + */ + CurrentLineReplaced = 'currentLineReplaced', + /** + * Expects the current line to be completed. + * + * Example: + * + * ``` + * path/to/file:{currentLineNumber} + * - class Foo + * + class Foo + * ``` + */ + CurrentLineCompleted = 'currentLineCompleted', +} + +export namespace PatchModelPrediction { + export const VALIDATOR = vEnum( + PatchModelPrediction.FilePath, + PatchModelPrediction.CurrentLine, + PatchModelPrediction.CurrentLineReplaced, + PatchModelPrediction.CurrentLineCompleted + ); +} + export interface ModelConfiguration { modelName: string; promptingStrategy: PromptingStrategy | undefined /* default */; @@ -696,6 +762,28 @@ export interface ModelConfiguration { supportsNextCursorLinePrediction?: boolean; /** Whether import-only edits are allowed. `undefined` is treated as {@link ImportChanges.None}. */ allowImportChanges?: ImportChanges; + /** Shape of the predicted output for patch-based responses. `undefined` falls back to the experiment default. */ + patchModelPredictionKind?: PatchModelPrediction; + /** Whether to split patches on diff boundaries. `undefined` falls back to the experiment default. */ + splitPatchOnDiff?: boolean; + /** Whether to fast-yield the line containing the cursor when parsing patches. `undefined` falls back to the experiment default. */ + patchFastYieldLineWithCursor?: boolean; + /** Extra debounce (ms) applied when the cursor is at the end of a line. `undefined` falls back to the experiment default. */ + extraDebounceEndOfLine?: number; + /** Whether cached suggestions should mimic ghost-text rendering behavior. `undefined` falls back to the experiment default. */ + nesMimicGhostTextBehavior?: boolean; + /** Minimum response delay (ms) enforced for cached edits. `undefined` falls back to the experiment default. */ + cacheDelay?: number; + /** Minimum response delay (ms) enforced for rebased cached edits. `undefined` falls back to the experiment default. */ + rebasedCacheDelay?: number; + /** Base debounce (ms) applied before issuing a request. `undefined` falls back to the experiment default. */ + debounce?: number; + /** + * Whether this model handles inline completions itself, so the separate completions provider + * should be suppressed and the client should run as the single unified provider. `undefined` + * falls back to the experiment/deployment toggles. + */ + supportsUnifiedCompletions?: boolean; } /** @@ -703,18 +791,34 @@ export interface ModelConfiguration { * declares values here, those values override anything provided by the upstream * model configuration. A strategy without an entry contributes no overrides. */ +const PATCH_BASED_02_WITH_RECENT_LINE_NUMBERS_CONFIG: Partial = { + includeTagsInCurrentFile: false, + includePostScript: true, + currentFile: { includeLineNumbers: IncludeLineNumbersOption.WithoutSpace }, + recentlyViewedDocuments: { includeLineNumbers: IncludeLineNumbersOption.WithoutSpace }, + supportsNextCursorLinePrediction: false, + allowImportChanges: ImportChanges.All, +}; + const STRATEGY_CONFIG: Partial>> = { // proxy /models doesn't know about includeTagsInCurrentFile field as of now, so hard-code it for CopilotNesXtab [PromptingStrategy.CopilotNesXtab]: { includeTagsInCurrentFile: true, }, - [PromptingStrategy.PatchBased02WithRecentLineNumbers]: { - includeTagsInCurrentFile: false, - includePostScript: true, - currentFile: { includeLineNumbers: IncludeLineNumbersOption.WithoutSpace }, - recentlyViewedDocuments: { includeLineNumbers: IncludeLineNumbersOption.WithoutSpace }, - supportsNextCursorLinePrediction: false, - allowImportChanges: ImportChanges.All, + [PromptingStrategy.PatchBased02WithRecentLineNumbers]: PATCH_BASED_02_WITH_RECENT_LINE_NUMBERS_CONFIG, + // Inherits everything from PatchBased02WithRecentLineNumbers and additionally bakes in the + // client/latency knobs that this unified model was tuned to run with. + [PromptingStrategy.PatchBased02Optimized]: { + ...PATCH_BASED_02_WITH_RECENT_LINE_NUMBERS_CONFIG, + patchModelPredictionKind: PatchModelPrediction.CurrentLineCompleted, + splitPatchOnDiff: true, + patchFastYieldLineWithCursor: true, + extraDebounceEndOfLine: 0, + nesMimicGhostTextBehavior: true, + cacheDelay: 200, + rebasedCacheDelay: 0, + debounce: 0, + supportsUnifiedCompletions: true, }, [PromptingStrategy.PatchBased02WithoutRecentLineNumbers]: { includeTagsInCurrentFile: false, @@ -765,6 +869,15 @@ export const MODEL_CONFIGURATION_VALIDATOR: IValidator = vOb 'lintOptions': vUnion(LINT_OPTIONS_VALIDATOR, vUndefined()), 'supportsNextCursorLinePrediction': vUnion(vBoolean(), vUndefined()), 'allowImportChanges': vUnion(ImportChanges.VALIDATOR, vUndefined()), + 'patchModelPredictionKind': vUnion(PatchModelPrediction.VALIDATOR, vUndefined()), + 'splitPatchOnDiff': vUnion(vBoolean(), vUndefined()), + 'patchFastYieldLineWithCursor': vUnion(vBoolean(), vUndefined()), + 'extraDebounceEndOfLine': vUnion(vNumber(), vUndefined()), + 'nesMimicGhostTextBehavior': vUnion(vBoolean(), vUndefined()), + 'cacheDelay': vUnion(vNumber(), vUndefined()), + 'rebasedCacheDelay': vUnion(vNumber(), vUndefined()), + 'debounce': vUnion(vNumber(), vUndefined()), + 'supportsUnifiedCompletions': vUnion(vBoolean(), vUndefined()), }); export function parseLintOptionString(optionString: string, defaults: LintOptions): LintOptions { @@ -977,66 +1090,3 @@ export enum SpeculativeRequestsAutoExpandEditWindowLines { export namespace SpeculativeRequestsAutoExpandEditWindowLines { export const VALIDATOR = vEnum(SpeculativeRequestsAutoExpandEditWindowLines.Off, SpeculativeRequestsAutoExpandEditWindowLines.Smart, SpeculativeRequestsAutoExpandEditWindowLines.Always); } - -/** - * Shape of the predicted output we send to the patch-based model along with the prompt. - * In every example below, `{currentLineNumber}` is 0-based — matching `Patch.lineNumZeroBased` - * parsed by `XtabCustomDiffPatchResponseHandler`. - */ -export enum PatchModelPrediction { - /** - * Expects changes in the current file but doesn't expect where (line number is not specified). - * - * Example: - * - * ``` - * path/to/file: - * ``` - */ - FilePath = 'filePath', - /** - * Predicts the file path, cursor line number, and a deletion of the current line. - * The model is free to follow with further `-`/`+` lines as needed. - * - * Example: - * - * ``` - * path/to/file:{currentLineNumber} - * - class Foo { - * ``` - */ - CurrentLine = 'currentLine', - /** - * Expects the current line to be replaced. - * - * Example: - * - * ``` - * path/to/file:{currentLineNumber} - * - class Foo { - * + - * ``` - */ - CurrentLineReplaced = 'currentLineReplaced', - /** - * Expects the current line to be completed. - * - * Example: - * - * ``` - * path/to/file:{currentLineNumber} - * - class Foo - * + class Foo - * ``` - */ - CurrentLineCompleted = 'currentLineCompleted', -} - -export namespace PatchModelPrediction { - export const VALIDATOR = vEnum( - PatchModelPrediction.FilePath, - PatchModelPrediction.CurrentLine, - PatchModelPrediction.CurrentLineReplaced, - PatchModelPrediction.CurrentLineCompleted - ); -} diff --git a/extensions/copilot/src/platform/inlineEdits/common/inlineEditsModelService.ts b/extensions/copilot/src/platform/inlineEdits/common/inlineEditsModelService.ts index 1fbfab45ef9c94..a01b62662407f9 100644 --- a/extensions/copilot/src/platform/inlineEdits/common/inlineEditsModelService.ts +++ b/extensions/copilot/src/platform/inlineEdits/common/inlineEditsModelService.ts @@ -24,6 +24,35 @@ export interface IInlineEditsModelService { export const IInlineEditsModelService = createServiceIdentifier('IInlineEditsModelService'); +export class NullInlineEditsModelService implements IInlineEditsModelService { + declare _serviceBrand: undefined; + + readonly modelInfo = undefined; + + readonly onModelListUpdated = Event.None; + + setCurrentModelId(_modelId: string): Promise { + return Promise.resolve(); + } + + selectedModelConfiguration(): ModelConfiguration { + return NullInlineEditsModelService._defaultConfiguration(); + } + + defaultModelConfiguration(): ModelConfiguration { + return NullInlineEditsModelService._defaultConfiguration(); + } + + private static _defaultConfiguration(): ModelConfiguration { + return { + modelName: '', + promptingStrategy: undefined, + includeTagsInCurrentFile: true, + lintOptions: undefined, + }; + } +} + export interface IUndesiredModelsManager { readonly _serviceBrand: undefined; readonly onDidChange: Event; diff --git a/extensions/copilot/src/platform/inlineEdits/node/inlineEditsModelService.ts b/extensions/copilot/src/platform/inlineEdits/node/inlineEditsModelService.ts index 3e1c7e7d95abfe..abd1b7a197ddee 100644 --- a/extensions/copilot/src/platform/inlineEdits/node/inlineEditsModelService.ts +++ b/extensions/copilot/src/platform/inlineEdits/node/inlineEditsModelService.ts @@ -21,7 +21,7 @@ import { IProxyModelsService } from '../../proxyModels/common/proxyModelsService import { IExperimentationService } from '../../telemetry/common/nullExperimentationService'; import { ITelemetryService } from '../../telemetry/common/telemetry'; import { WireTypes } from '../common/dataTypes/inlineEditsModelsTypes'; -import { isPromptingStrategy, MODEL_CONFIGURATION_VALIDATOR, ModelConfiguration, PromptingStrategy } from '../common/dataTypes/xtabPromptOptions'; +import { applyStrategyConfig, isPromptingStrategy, MODEL_CONFIGURATION_VALIDATOR, ModelConfiguration, PromptingStrategy } from '../common/dataTypes/xtabPromptOptions'; import { IInlineEditsModelService, IUndesiredModelsManager } from '../common/inlineEditsModelService'; const enum ModelSource { @@ -284,7 +284,7 @@ export class InlineEditsModelService extends Disposable implements IInlineEditsM } public selectedModelConfiguration(): ModelConfiguration { - return toModelConfiguration(this._currentModelObs.get()); + return applyStrategyConfig(toModelConfiguration(this._currentModelObs.get())); } public defaultModelConfiguration(): ModelConfiguration { diff --git a/extensions/copilot/src/platform/inlineEdits/test/common/xtabPromptOptions.spec.ts b/extensions/copilot/src/platform/inlineEdits/test/common/xtabPromptOptions.spec.ts index d6a6d7f41a3751..fde80642641a25 100644 --- a/extensions/copilot/src/platform/inlineEdits/test/common/xtabPromptOptions.spec.ts +++ b/extensions/copilot/src/platform/inlineEdits/test/common/xtabPromptOptions.spec.ts @@ -5,7 +5,7 @@ import { describe, expect, it } from 'vitest'; import { ImportChanges } from '../../common/dataTypes/importFilteringOptions'; -import { applyStrategyConfig, DEFAULT_OPTIONS, GlobalBudgetOptions, IncludeLineNumbersOption, isEagernessPrompt, MODEL_CONFIGURATION_VALIDATOR, ModelConfiguration, PromptingStrategy, RejectedEditsMemoryMode } from '../../common/dataTypes/xtabPromptOptions'; +import { applyStrategyConfig, DEFAULT_OPTIONS, GlobalBudgetOptions, IncludeLineNumbersOption, isEagernessPrompt, MODEL_CONFIGURATION_VALIDATOR, ModelConfiguration, PatchModelPrediction, PromptingStrategy, RejectedEditsMemoryMode } from '../../common/dataTypes/xtabPromptOptions'; function baseConfig(overrides: Partial = {}): ModelConfiguration { return { @@ -53,6 +53,40 @@ describe('applyStrategyConfig', () => { recentlyViewedDocuments: { includeLineNumbers: IncludeLineNumbersOption.WithoutSpace, maxTokens: 99 }, supportsNextCursorLinePrediction: false, }); + // The additional client/latency knobs are only baked into PatchBased02Optimized. + expect(result.patchModelPredictionKind).toBeUndefined(); + expect(result.cacheDelay).toBeUndefined(); + expect(result.debounce).toBeUndefined(); + expect(result.supportsUnifiedCompletions).toBeUndefined(); + }); + + it('forces baked-in fields for PatchBased02Optimized', () => { + const result = applyStrategyConfig(baseConfig({ + promptingStrategy: PromptingStrategy.PatchBased02Optimized, + includeTagsInCurrentFile: true, + includePostScript: false, + currentFile: { includeLineNumbers: IncludeLineNumbersOption.None, maxTokens: 42 }, + recentlyViewedDocuments: { includeLineNumbers: IncludeLineNumbersOption.None, maxTokens: 99 }, + supportsNextCursorLinePrediction: true, + allowImportChanges: ImportChanges.None, + })); + expect(result).toMatchObject({ + includeTagsInCurrentFile: false, + includePostScript: true, + currentFile: { includeLineNumbers: IncludeLineNumbersOption.WithoutSpace, maxTokens: 42 }, + recentlyViewedDocuments: { includeLineNumbers: IncludeLineNumbersOption.WithoutSpace, maxTokens: 99 }, + supportsNextCursorLinePrediction: false, + allowImportChanges: ImportChanges.All, + patchModelPredictionKind: PatchModelPrediction.CurrentLineCompleted, + splitPatchOnDiff: true, + patchFastYieldLineWithCursor: true, + extraDebounceEndOfLine: 0, + nesMimicGhostTextBehavior: true, + cacheDelay: 200, + rebasedCacheDelay: 0, + debounce: 0, + supportsUnifiedCompletions: true, + }); }); it('forces recentlyViewedDocuments.includeLineNumbers=None for PatchBased02WithoutRecentLineNumbers', () => {