Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/endpoint/test/node/openaiCompatibleEndpoint.ts
13405 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { OpenAI } from '@vscode/prompt-tsx';
7
import { TokenizerType } from '../../../../util/common/tokenizer';
8
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
9
import { IAuthenticationService } from '../../../authentication/common/authentication';
10
import { IChatMLFetcher } from '../../../chat/common/chatMLFetcher';
11
import { IConfigurationService } from '../../../configuration/common/configurationService';
12
import { IEnvService } from '../../../env/common/envService';
13
import { ILogService } from '../../../log/common/logService';
14
import { isOpenAiFunctionTool } from '../../../networking/common/fetch';
15
import { IFetcherService } from '../../../networking/common/fetcherService';
16
import { IChatEndpoint, ICreateEndpointBodyOptions, IEndpointBody } from '../../../networking/common/networking';
17
import { CAPIChatMessage, RawMessageConversionCallback } from '../../../networking/common/openai';
18
import { IChatWebSocketManager } from '../../../networking/node/chatWebSocketManager';
19
import { IExperimentationService } from '../../../telemetry/common/nullExperimentationService';
20
import { ITelemetryService } from '../../../telemetry/common/telemetry';
21
import { ITokenizerProvider } from '../../../tokenizer/node/tokenizer';
22
import { ICAPIClientService } from '../../common/capiClient';
23
import { IDomainService } from '../../common/domainService';
24
import { IChatModelInformation, ModelSupportedEndpoint } from '../../common/endpointProvider';
25
import { ChatEndpoint } from '../../node/chatEndpoint';
26
27
export type IModelConfig = {
28
id: string;
29
name: string;
30
version: string;
31
useDeveloperRole: boolean;
32
type: 'openai' | 'azureOpenai';
33
capabilities: {
34
supports: {
35
parallel_tool_calls: boolean;
36
streaming: boolean;
37
tool_calls: boolean;
38
vision: boolean;
39
prediction: boolean;
40
thinking: boolean;
41
};
42
limits: {
43
max_prompt_tokens: number;
44
max_output_tokens: number;
45
max_context_window_tokens?: number;
46
};
47
};
48
supported_endpoints: readonly ModelSupportedEndpoint[];
49
url: string;
50
auth: {
51
/**
52
* Use Bearer token for authentication
53
*/
54
useBearerHeader: boolean;
55
/**
56
* Use API key for authentication
57
*/
58
useApiKeyHeader: boolean;
59
/**
60
* The environment variable name for the API key
61
*/
62
apiKeyEnvName?: string;
63
};
64
overrides: {
65
requestHeaders: Record<string, string>;
66
// If any value is set to null, it will be deleted from the request body
67
// if the value is undefined, it will not override any existing value in the request body
68
// if the value is set, it will override the existing value in the request body
69
temperature?: number | null;
70
top_p?: number | null;
71
snippy?: boolean | null;
72
max_tokens?: number | null;
73
max_completion_tokens?: number | null;
74
intent?: boolean | null;
75
};
76
};
77
78
export class OpenAICompatibleTestEndpoint extends ChatEndpoint {
79
constructor(
80
private readonly modelConfig: IModelConfig,
81
@IDomainService domainService: IDomainService,
82
@ICAPIClientService capiClientService: ICAPIClientService,
83
@IFetcherService fetcherService: IFetcherService,
84
@IEnvService envService: IEnvService,
85
@ITelemetryService telemetryService: ITelemetryService,
86
@IAuthenticationService authService: IAuthenticationService,
87
@IChatMLFetcher chatMLFetcher: IChatMLFetcher,
88
@ITokenizerProvider tokenizerProvider: ITokenizerProvider,
89
@IInstantiationService private instantiationService: IInstantiationService,
90
@IConfigurationService configurationService: IConfigurationService,
91
@IExperimentationService experimentationService: IExperimentationService,
92
@IChatWebSocketManager chatWebSocketService: IChatWebSocketManager,
93
@ILogService logService: ILogService
94
) {
95
const modelInfo: IChatModelInformation = {
96
id: modelConfig.id,
97
vendor: 'OpenAI Compatible',
98
name: modelConfig.name,
99
version: modelConfig.version,
100
model_picker_enabled: false,
101
is_chat_default: false,
102
is_chat_fallback: false,
103
capabilities: {
104
type: 'chat',
105
family: modelConfig.type === 'azureOpenai' ? 'azure' : 'openai',
106
tokenizer: TokenizerType.O200K,
107
supports: {
108
parallel_tool_calls: modelConfig.capabilities.supports.parallel_tool_calls,
109
streaming: modelConfig.capabilities.supports.streaming,
110
tool_calls: modelConfig.capabilities.supports.tool_calls,
111
vision: modelConfig.capabilities.supports.vision,
112
prediction: modelConfig.capabilities.supports.prediction,
113
thinking: modelConfig.capabilities.supports.thinking ?? false
114
},
115
limits: {
116
max_prompt_tokens: modelConfig.capabilities.limits.max_prompt_tokens,
117
max_output_tokens: modelConfig.capabilities.limits.max_output_tokens,
118
max_context_window_tokens: modelConfig.capabilities.limits.max_context_window_tokens
119
}
120
},
121
supported_endpoints: Array.isArray(modelConfig.supported_endpoints) && modelConfig.supported_endpoints.length > 0
122
? modelConfig.supported_endpoints
123
: [ModelSupportedEndpoint.ChatCompletions]
124
};
125
126
super(
127
modelInfo,
128
domainService,
129
chatMLFetcher,
130
tokenizerProvider,
131
instantiationService,
132
configurationService,
133
experimentationService,
134
chatWebSocketService,
135
logService
136
);
137
}
138
139
override get urlOrRequestMetadata(): string {
140
return this.modelConfig.version ? this.modelConfig.url + '?api-version=' + this.modelConfig.version : this.modelConfig.url;
141
}
142
143
public override getExtraHeaders(): Record<string, string> {
144
const headers: Record<string, string> = {
145
'Content-Type': 'application/json'
146
};
147
148
if (this.modelConfig.auth.useBearerHeader || this.modelConfig.auth.useApiKeyHeader) {
149
if (!this.modelConfig.auth.apiKeyEnvName) {
150
throw new Error('API key environment variable name is not set in the model configuration');
151
}
152
const apiKey = process.env[this.modelConfig.auth.apiKeyEnvName];
153
if (!apiKey) {
154
throw new Error(`API key environment variable ${this.modelConfig.auth.apiKeyEnvName} is not set`);
155
}
156
157
if (this.modelConfig.auth.useBearerHeader) {
158
headers['Authorization'] = `Bearer ${apiKey}`;
159
}
160
161
if (this.modelConfig.auth.useApiKeyHeader) {
162
headers['api-key'] = apiKey;
163
}
164
}
165
166
if (this.modelConfig.overrides.requestHeaders) {
167
Object.entries(this.modelConfig.overrides.requestHeaders).forEach(([key, value]) => {
168
headers[key] = value;
169
});
170
}
171
172
return headers;
173
}
174
175
override createRequestBody(options: ICreateEndpointBodyOptions): IEndpointBody {
176
if (this.useResponsesApi) {
177
// Handle Responses API: customize the body directly
178
options.ignoreStatefulMarker = false;
179
const body = super.createRequestBody(options);
180
body.store = true;
181
body.n = undefined;
182
body.stream_options = undefined;
183
if (!this.modelConfig.capabilities.supports.thinking) {
184
body.reasoning = undefined;
185
}
186
return body;
187
}
188
const body = super.createRequestBody(options);
189
return body;
190
}
191
192
override interceptBody(body: IEndpointBody | undefined): void {
193
super.interceptBody(body);
194
195
if (body?.tools?.length === 0) {
196
delete body.tools;
197
}
198
199
if (body?.messages) {
200
body.messages.forEach((message: any) => {
201
if (message.copilot_cache_control) {
202
delete message.copilot_cache_control;
203
}
204
});
205
}
206
207
if (body) {
208
if (this.modelConfig.overrides.snippy === null) {
209
delete body.snippy;
210
} else if (this.modelConfig.overrides.snippy) {
211
body.snippy = { enabled: this.modelConfig.overrides.snippy };
212
}
213
214
if (this.modelConfig.overrides.intent === null) {
215
delete body.intent;
216
} else if (this.modelConfig.overrides.intent) {
217
body.intent = this.modelConfig.overrides.intent;
218
}
219
220
if (this.modelConfig.overrides.temperature === null) {
221
delete body.temperature;
222
} else if (this.modelConfig.overrides.temperature) {
223
body.temperature = this.modelConfig.overrides.temperature;
224
}
225
226
if (this.modelConfig.overrides.top_p === null) {
227
delete body.top_p;
228
} else if (this.modelConfig.overrides.top_p) {
229
body.top_p = this.modelConfig.overrides.top_p;
230
}
231
232
if (this.modelConfig.overrides.max_tokens === null) {
233
delete body.max_tokens;
234
} else if (this.modelConfig.overrides.max_tokens) {
235
body.max_tokens = this.modelConfig.overrides.max_tokens;
236
}
237
}
238
239
if (body?.tools) {
240
body.tools = body.tools.map(tool => {
241
if (isOpenAiFunctionTool(tool) && tool.function.parameters === undefined) {
242
tool.function.parameters = { type: 'object', properties: {} };
243
}
244
return tool;
245
});
246
}
247
248
if (this.modelConfig.type === 'openai') {
249
if (body) {
250
if (!this.useResponsesApi) {
251
// we need to set this to unsure usage stats are logged
252
body['stream_options'] = { 'include_usage': true };
253
}
254
// OpenAI requires the model name to be set in the body
255
body.model = this.modelConfig.name;
256
257
// Handle messages reformatting if messages exist
258
if (body.messages) {
259
const newMessages: CAPIChatMessage[] = body.messages.map((message: CAPIChatMessage): CAPIChatMessage => {
260
if (message.role === OpenAI.ChatRole.System) {
261
return {
262
role: OpenAI.ChatRole.User,
263
content: message.content,
264
};
265
} else {
266
return message;
267
}
268
});
269
body['messages'] = newMessages;
270
}
271
}
272
}
273
274
if (this.modelConfig.useDeveloperRole && body) {
275
const newMessages = body.messages!.map((message: CAPIChatMessage) => {
276
if (message.role === OpenAI.ChatRole.System) {
277
return { role: 'developer' as OpenAI.ChatRole.System, content: message.content };
278
}
279
return message;
280
});
281
Object.keys(body).forEach(key => delete (body as any)[key]);
282
body.messages = newMessages;
283
}
284
}
285
286
override cloneWithTokenOverride(_modelMaxPromptTokens: number): IChatEndpoint {
287
return this.instantiationService.createInstance(OpenAICompatibleTestEndpoint, this.modelConfig);
288
}
289
290
protected override getCompletionsCallback(): RawMessageConversionCallback | undefined {
291
return (out, data) => {
292
if (data && data.id) {
293
out.cot_id = data.id;
294
out.cot_summary = Array.isArray(data.text) ? data.text.join('') : data.text;
295
}
296
};
297
}
298
}
299
300