Skip to content

Commit e361b5e

Browse files
authored
edits: allow oai config and byok api config for edit tools (#1161)
* edits: allow oai config and byok api config for edit tools Adds on to #1154 and builds on microsoft/vscode#268506 Allows 3p providers to configure preferred edit tools for endpoints (once the API is finalized) and uses that to allow the edit tools to be configured in `github.copilot.chat.customOAIModels` for power users. * comment * fix gdpr comment
1 parent afc5b99 commit e361b5e

14 files changed

+136
-33
lines changed

package.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2997,6 +2997,19 @@
29972997
"description": "Whether the model requires an API key for authentication",
29982998
"default": true
29992999
},
3000+
"editTools": {
3001+
"type": "array",
3002+
"description": "List of edit tools supported by the model. If this is not configured, the editor will try multiple edit tools and pick the best one.\n\n- 'find-replace': Find and replace text in a document.\n- 'multi-find-replace': Find and replace text in a document.\n- 'apply-patch': A file-oriented diff format used by some OpenAI models\n- 'code-rewrite': A general but slower editing tool that allows the model to rewrite and code snippet and provide only the replacement to the editor.",
3003+
"items": {
3004+
"type": "string",
3005+
"enum": [
3006+
"find-replace",
3007+
"multi-find-replace",
3008+
"apply-patch",
3009+
"code-rewrite"
3010+
]
3011+
}
3012+
},
30003013
"thinking": {
30013014
"type": "boolean",
30023015
"default": false,

src/extension/byok/common/byokProvider.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import type { Disposable, LanguageModelChatInformation, LanguageModelChatProvider, LanguageModelDataPart, LanguageModelTextPart, LanguageModelThinkingPart, LanguageModelToolCallPart } from 'vscode';
66
import { CopilotToken } from '../../../platform/authentication/common/copilotToken';
77
import { ICAPIClientService } from '../../../platform/endpoint/common/capiClient';
8-
import { IChatModelInformation } from '../../../platform/endpoint/common/endpointProvider';
8+
import { EndpointEditToolName, IChatModelInformation } from '../../../platform/endpoint/common/endpointProvider';
99
import { TokenizerType } from '../../../util/common/tokenizer';
1010
import { localize } from '../../../util/vs/nls';
1111

@@ -54,6 +54,7 @@ export interface BYOKModelCapabilities {
5454
toolCalling: boolean;
5555
vision: boolean;
5656
thinking?: boolean;
57+
editTools?: EndpointEditToolName[];
5758
}
5859

5960
export interface BYOKModelRegistry {

src/extension/byok/vscode-node/customOAIModelConfigurator.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { InputBoxOptions, LanguageModelChatInformation, QuickInputButtons, QuickPickItem, window } from 'vscode';
77
import { Config, ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
8+
import { EndpointEditToolName } from '../../../platform/endpoint/common/endpointProvider';
89
import { DisposableStore } from '../../../util/vs/base/common/lifecycle';
910
import { BYOKModelProvider } from '../common/byokProvider';
1011

@@ -15,6 +16,7 @@ interface ModelConfig {
1516
vision: boolean;
1617
maxInputTokens: number;
1718
maxOutputTokens: number;
19+
editTools?: EndpointEditToolName[];
1820
requiresAPIKey?: boolean;
1921
thinking?: boolean;
2022
}
@@ -414,6 +416,7 @@ export class CustomOAIModelConfigurator {
414416
}
415417

416418
return {
419+
...currentConfig,
417420
name: modelName.trim(),
418421
url: url.trim(),
419422
toolCalling: capabilities.toolCalling,

src/extension/byok/vscode-node/customOAIProvider.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { CancellationToken, LanguageModelChatInformation, LanguageModelChatMessage, LanguageModelChatMessage2, LanguageModelResponsePart2, Progress, ProvideLanguageModelChatResponseOptions, QuickPickItem, window } from 'vscode';
77
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
8+
import { EndpointEditToolName, isEndpointEditToolName } from '../../../platform/endpoint/common/endpointProvider';
89
import { ILogService } from '../../../platform/log/common/logService';
910
import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';
1011
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
@@ -66,8 +67,8 @@ export class CustomOAIBYOKModelProvider implements BYOKModelProvider<CustomOAIMo
6667
return resolveCustomOAIUrl(modelId, url);
6768
}
6869

69-
private getUserModelConfig(): Record<string, { name: string; url: string; toolCalling: boolean; vision: boolean; maxInputTokens: number; maxOutputTokens: number; requiresAPIKey: boolean; thinking?: boolean }> {
70-
const modelConfig = this._configurationService.getConfig(this.getConfigKey()) as Record<string, { name: string; url: string; toolCalling: boolean; vision: boolean; maxInputTokens: number; maxOutputTokens: number; requiresAPIKey: boolean; thinking?: boolean }>;
70+
private getUserModelConfig(): Record<string, { name: string; url: string; toolCalling: boolean; vision: boolean; maxInputTokens: number; maxOutputTokens: number; requiresAPIKey: boolean; thinking?: boolean; editTools?: EndpointEditToolName[] }> {
71+
const modelConfig = this._configurationService.getConfig(this.getConfigKey()) as Record<string, { name: string; url: string; toolCalling: boolean; vision: boolean; maxInputTokens: number; maxOutputTokens: number; requiresAPIKey: boolean; thinking?: boolean; editTools?: EndpointEditToolName[] }>;
7172
return modelConfig;
7273
}
7374

@@ -88,6 +89,7 @@ export class CustomOAIBYOKModelProvider implements BYOKModelProvider<CustomOAIMo
8889
maxInputTokens: modelInfo.maxInputTokens,
8990
maxOutputTokens: modelInfo.maxOutputTokens,
9091
thinking: modelInfo.thinking,
92+
editTools: modelInfo.editTools,
9193
};
9294
}
9395
return models;
@@ -129,7 +131,8 @@ export class CustomOAIBYOKModelProvider implements BYOKModelProvider<CustomOAIMo
129131
tooltip: `${capabilities.name} is contributed via the ${this.providerName} provider.`,
130132
capabilities: {
131133
toolCalling: capabilities.toolCalling,
132-
imageInput: capabilities.vision
134+
imageInput: capabilities.vision,
135+
editTools: capabilities.editTools
133136
},
134137
thinking: capabilities.thinking || false,
135138
};
@@ -168,7 +171,8 @@ export class CustomOAIBYOKModelProvider implements BYOKModelProvider<CustomOAIMo
168171
vision: !!model.capabilities?.imageInput || false,
169172
name: model.name,
170173
url: model.url,
171-
thinking: model.thinking
174+
thinking: model.thinking,
175+
editTools: model.capabilities.editTools?.filter(isEndpointEditToolName),
172176
});
173177
const openAIChatEndpoint = this._instantiationService.createInstance(OpenAIEndpoint, modelInfo, apiKey ?? '', model.url);
174178
return this._lmWrapper.provideLanguageModelResponse(openAIChatEndpoint, messages, options, options.requestInitiator, progress, token);

src/extension/tools/common/editToolLearningService.ts

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import type { LanguageModelChat } from 'vscode';
77
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
88
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
99
import { IChatEndpoint } from '../../../platform/networking/common/networking';
10+
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
1011
import { createServiceIdentifier } from '../../../util/common/services';
1112
import { LRUCache } from '../../../util/vs/base/common/map';
1213
import { mapValues } from '../../../util/vs/base/common/objects';
14+
import { isDefined } from '../../../util/vs/base/common/types';
1315
import { EditTools as _EditTools, EDIT_TOOL_LEARNING_STATES, IEditToolLearningData, LearningConfig, State } from './editToolLearningStates';
14-
import { ToolName } from './toolNames';
16+
import { byokEditToolNamesToToolNames, ToolName } from './toolNames';
1517

1618
export type EditTools = _EditTools;
1719

@@ -49,6 +51,7 @@ export class EditToolLearningService implements IEditToolLearningService {
4951
constructor(
5052
@IVSCodeExtensionContext private readonly _context: IVSCodeExtensionContext,
5153
@IEndpointProvider private readonly _endpointProvider: IEndpointProvider,
54+
@ITelemetryService private readonly _telemetryService: ITelemetryService,
5255
) { }
5356

5457
async getPreferredEditTool(model: LanguageModelChat): Promise<EditTools[] | undefined> {
@@ -61,7 +64,16 @@ export class EditToolLearningService implements IEditToolLearningService {
6164
return undefined;
6265
}
6366

64-
const hardcoded = this._getHardcodedPreferences(endpoint.model);
67+
const fromEndpoint = endpoint.supportedEditTools
68+
?.map(e => byokEditToolNamesToToolNames.hasOwnProperty(e) ? byokEditToolNamesToToolNames[e] : undefined)
69+
.filter(isDefined);
70+
if (fromEndpoint?.length) {
71+
return fromEndpoint;
72+
}
73+
74+
// Note: looking at the 'name' rather than 'model' is intentional, 'model' is the user-
75+
// provided model ID whereas the 'name' is the name of the model on the BYOK provider.
76+
const hardcoded = this._getHardcodedPreferences(endpoint.name);
6577
if (hardcoded) {
6678
return hardcoded;
6779
}
@@ -78,7 +90,7 @@ export class EditToolLearningService implements IEditToolLearningService {
7890
}
7991

8092
const learningData = this._getModelLearningData(model.id);
81-
this._recordEdit(learningData, tool, success);
93+
this._recordEdit(model.id, learningData, tool, success);
8294
await this._saveModelLearningData(model.id, learningData);
8395
}
8496

@@ -100,25 +112,45 @@ export class EditToolLearningService implements IEditToolLearningService {
100112
return EDIT_TOOL_LEARNING_STATES[data.state].allowedTools;
101113
}
102114

103-
private _checkStateTransitions(data: IEditToolLearningData): State {
115+
private _checkStateTransitions(modelId: string, data: IEditToolLearningData): State {
104116
const currentConfig = EDIT_TOOL_LEARNING_STATES[data.state];
105117

118+
if (!currentConfig.transitions) {
119+
return data.state;
120+
}
121+
106122
for (const [targetState, condition] of Object.entries(currentConfig.transitions)) {
107-
if (condition(data)) {
108-
return Number(targetState) as State;
123+
if (!condition(data)) {
124+
continue;
109125
}
126+
127+
const target = Number(targetState) as State;
128+
129+
/* __GDPR__
130+
"editToolLearning.transition" : {
131+
"owner": "connor4312",
132+
"comment": "Tracks state transitions in the edit tool learning system.",
133+
"modelId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "Model ID" },
134+
"state": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "State the model transitioned to", "isMeasurement": true }
135+
}
136+
*/
137+
this._telemetryService.sendMSFTTelemetryEvent('editToolLearning.transition', { modelId }, {
138+
state: target,
139+
});
140+
141+
return target;
110142
}
111143

112144
return data.state; // No transition
113145
}
114146

115-
private _recordEdit(data: IEditToolLearningData, tool: EditTools, success: boolean): void {
147+
private _recordEdit(modelId: string, data: IEditToolLearningData, tool: EditTools, success: boolean): void {
116148
const successBit = success ? 1n : 0n;
117149
const toolData = (data.tools[tool] ??= { successBitset: 0n, attempts: 0 });
118150
toolData.successBitset = addToWindow(toolData.successBitset, successBit);
119151
toolData.attempts++;
120152

121-
const newState = this._checkStateTransitions(data);
153+
const newState = this._checkStateTransitions(modelId, data);
122154
if (newState !== data.state) {
123155
data.state = newState;
124156
data.tools = {};

src/extension/tools/common/editToolLearningStates.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export const enum State {
4242

4343
interface StateConfig {
4444
allowedTools: EditTools[];
45-
transitions: { [K in State]?: (data: IEditToolLearningData) => boolean };
45+
transitions?: { [K in State]?: (data: IEditToolLearningData) => boolean };
4646
}
4747

4848

@@ -126,14 +126,11 @@ export const EDIT_TOOL_LEARNING_STATES: Record<State, StateConfig> = {
126126
// Terminal states have no transitions
127127
[State.EditFileOnly]: {
128128
allowedTools: [ToolName.EditFile],
129-
transitions: {},
130129
},
131130
[State.ReplaceStringOnly]: {
132131
allowedTools: [ToolName.ReplaceString],
133-
transitions: {},
134132
},
135133
[State.ReplaceStringWithMulti]: {
136134
allowedTools: [ToolName.ReplaceString, ToolName.MultiReplaceString],
137-
transitions: {},
138135
},
139136
};

src/extension/tools/common/toolNames.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,13 @@ export enum ContributedToolName {
103103
ExecuteTask = 'execute_task',
104104
}
105105

106+
export const byokEditToolNamesToToolNames = {
107+
'find-replace': ToolName.ReplaceString,
108+
'multi-find-replace': ToolName.MultiReplaceString,
109+
'apply-patch': ToolName.ApplyPatch,
110+
'code-rewrite': ToolName.EditFile,
111+
} as const;
112+
106113
const toolNameToContributedToolNames = new Map<ToolName, ContributedToolName>();
107114
const contributedToolNameToToolNames = new Map<ContributedToolName, ToolName>();
108115
for (const [contributedNameKey, contributedName] of Object.entries(ContributedToolName)) {

src/extension/tools/node/test/editToolLearningService.spec.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
77
import type { LanguageModelChat } from 'vscode';
88
import { IEndpointProvider } from '../../../../platform/endpoint/common/endpointProvider';
99
import { IChatEndpoint } from '../../../../platform/networking/common/networking';
10+
import { NullTelemetryService } from '../../../../platform/telemetry/common/nullTelemetryService';
1011
import { MockExtensionContext } from '../../../../platform/test/node/extensionContext';
1112
import { EditToolLearningService, EditTools } from '../../common/editToolLearningService';
1213
import { LearningConfig } from '../../common/editToolLearningStates';
@@ -48,7 +49,7 @@ describe('EditToolLearningService', () => {
4849
policy: 'enabled',
4950
urlOrRequestMetadata: 'test-url',
5051
modelMaxPromptTokens: 4000,
51-
name: 'test-model',
52+
name: model.id,
5253
version: '1.0',
5354
tokenizer: 'gpt',
5455
acceptChatPolicy: vi.fn().mockResolvedValue(true),
@@ -76,7 +77,7 @@ describe('EditToolLearningService', () => {
7677
// Set up proper spies for global state methods
7778
mockContext.globalState.get = vi.fn().mockReturnValue(undefined);
7879
mockContext.globalState.update = vi.fn().mockResolvedValue(undefined);
79-
service = new EditToolLearningService(mockContext as any, mockEndpointProvider);
80+
service = new EditToolLearningService(mockContext as any, mockEndpointProvider, new NullTelemetryService());
8081
});
8182

8283
describe('getPreferredEditTool', () => {
@@ -271,7 +272,7 @@ describe('EditToolLearningService', () => {
271272
await simulateEdits(model, ToolName.ReplaceString, 10, 5);
272273

273274
// Create a new service instance with the same context
274-
const newService = new EditToolLearningService(mockContext as any, mockEndpointProvider);
275+
const newService = new EditToolLearningService(mockContext as any, mockEndpointProvider, new NullTelemetryService());
275276

276277
// The new service should have access to the persisted data
277278
const result = await newService.getPreferredEditTool(model);

src/extension/vscode.d.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20454,20 +20454,24 @@ declare module 'vscode' {
2045420454
/**
2045520455
* Various features that the model supports such as tool calling or image input.
2045620456
*/
20457-
readonly capabilities: {
20457+
readonly capabilities: LanguageModelChatCapabilities;
20458+
}
2045820459

20459-
/**
20460-
* Whether image input is supported by the model.
20461-
* Common supported images are jpg and png, but each model will vary in supported mimetypes.
20462-
*/
20463-
readonly imageInput?: boolean;
20460+
/**
20461+
* Various features that the {@link LanguageModelChatInformation} supports such as tool calling or image input.
20462+
*/
20463+
export interface LanguageModelChatCapabilities {
20464+
/**
20465+
* Whether image input is supported by the model.
20466+
* Common supported images are jpg and png, but each model will vary in supported mimetypes.
20467+
*/
20468+
readonly imageInput?: boolean;
2046420469

20465-
/**
20466-
* Whether tool calling is supported by the model.
20467-
* If a number is provided, that is the maximum number of tools that can be provided in a request to the model.
20468-
*/
20469-
readonly toolCalling?: boolean | number;
20470-
};
20470+
/**
20471+
* Whether tool calling is supported by the model.
20472+
* If a number is provided, that is the maximum number of tools that can be provided in a request to the model.
20473+
*/
20474+
readonly toolCalling?: boolean | number;
2047120475
}
2047220476

2047320477
/**

src/extension/vscode.proposed.chatProvider.d.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,25 @@ declare module 'vscode' {
5656
readonly statusIcon?: ThemeIcon;
5757
}
5858

59+
export interface LanguageModelChatCapabilities {
60+
/**
61+
* The tools the model prefers for making file edits. If not provided or if none of the tools,
62+
* are recognized, the editor will try multiple edit tools and pick the best one. The available
63+
* edit tools WILL change over time and this capability only serves as a hint to the editor.
64+
*
65+
* Edit tools currently recognized include:
66+
* - 'find-replace': Find and replace text in a document.
67+
* - 'multi-find-replace': Find and replace text in a document.
68+
* - 'apply-patch': A file-oriented diff format used by some OpenAI models
69+
* - 'code-rewrite': A general but slower editing tool that allows the model
70+
* to rewrite and code snippet and provide only the replacement to the editor.
71+
*
72+
* The order of edit tools in this array has no significance; all of the recognized edit
73+
* tools will be made available to the model.
74+
*/
75+
readonly editTools?: string[];
76+
}
77+
5978
export type LanguageModelResponsePart2 = LanguageModelResponsePart | LanguageModelDataPart | LanguageModelThinkingPart;
6079

6180
export interface LanguageModelChatProvider<T extends LanguageModelChatInformation = LanguageModelChatInformation> {

0 commit comments

Comments
 (0)