Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/nesFetch/node/completionsFetchServiceImpl.ts
13401 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 { AsyncIterUtilsExt } from '../../../util/common/asyncIterableUtils';
7
import { ErrorUtils } from '../../../util/common/errors';
8
import { Result } from '../../../util/common/result';
9
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
10
import { Codicon } from '../../../util/vs/base/common/codicons';
11
import { IDisposable } from '../../../util/vs/base/common/lifecycle';
12
import { ThemeIcon } from '../../../util/vs/base/common/themables';
13
import { IAuthenticationService } from '../../authentication/common/authentication';
14
import { getRequestId, RequestId } from '../../networking/common/fetch';
15
import { FetchOptions, IFetcherService, IHeaders, Response } from '../../networking/common/fetcherService';
16
import { IRequestLogger, LoggedRequestKind } from '../../requestLogger/common/requestLogger';
17
import { Completion } from '../common/completionsAPI';
18
import { Completions, ICompletionsFetchService } from '../common/completionsFetchService';
19
import { ResponseStream } from '../common/responseStream';
20
import { jsonlStreamToCompletions } from './streamTransformer';
21
22
export type FetchResponse = {
23
status: number;
24
statusText: string;
25
headers: IHeaders;
26
body: AsyncIterable<string>;
27
requestId: RequestId;
28
response: Response;
29
};
30
31
export interface IFetchRequestParams extends Completions.ModelParams { }
32
33
export class CompletionsFetchService implements ICompletionsFetchService {
34
readonly _serviceBrand: undefined;
35
36
constructor(
37
@IAuthenticationService private authService: IAuthenticationService,
38
@IFetcherService private fetcherService: IFetcherService,
39
@IRequestLogger private readonly requestLogger: IRequestLogger,
40
) {
41
}
42
43
public disconnectAll(): Promise<unknown> {
44
return this.fetcherService.disconnectAll();
45
}
46
47
public async fetch(
48
url: string,
49
secretKey: string,
50
params: IFetchRequestParams,
51
requestId: string,
52
ct: CancellationToken,
53
headerOverrides?: Record<string, string>,
54
): Promise<Result<ResponseStream, Completions.CompletionsFetchFailure>> {
55
const startTimeMs = Date.now();
56
57
if (ct.isCancellationRequested) {
58
const result = Result.error(new Completions.RequestCancelled());
59
this._logCompletionsRequest(url, params, requestId, startTimeMs, result);
60
return result;
61
}
62
63
const options = {
64
requestId,
65
headers: this.getHeaders(requestId, secretKey, headerOverrides),
66
body: JSON.stringify({
67
...params,
68
stream: true,
69
})
70
};
71
72
const fetchResponse = await this._fetchFromUrl(url, options, ct);
73
74
if (fetchResponse.isError()) {
75
this._logCompletionsRequest(url, params, requestId, startTimeMs, fetchResponse);
76
return fetchResponse;
77
}
78
79
if (fetchResponse.val.status === 200) {
80
81
const jsonlStream = AsyncIterUtilsExt.splitLines(fetchResponse.val.body);
82
const completionsStream = jsonlStreamToCompletions(jsonlStream);
83
84
const response = new ResponseStream(fetchResponse.val.response, completionsStream, fetchResponse.val.requestId, fetchResponse.val.headers);
85
86
const result = Result.ok(response);
87
this._logCompletionsRequest(url, params, requestId, startTimeMs, result);
88
return result;
89
90
} else {
91
const error: Completions.CompletionsFetchFailure = new Completions.UnsuccessfulResponse(
92
fetchResponse.val.status,
93
fetchResponse.val.statusText,
94
fetchResponse.val.headers,
95
() => collectAsyncIterableToString(fetchResponse.val.body).catch(() => ''),
96
);
97
98
const result = Result.error(error);
99
this._logCompletionsRequest(url, params, requestId, startTimeMs, result);
100
return result;
101
}
102
}
103
104
protected async _fetchFromUrl(url: string, options: Completions.Internal.FetchOptions, ct: CancellationToken): Promise<Result<FetchResponse, Completions.CompletionsFetchFailure>> {
105
106
const fetchAbortCtl = this.fetcherService.makeAbortController();
107
108
const onCancellationDisposable = ct.onCancellationRequested(() => {
109
fetchAbortCtl.abort();
110
});
111
112
try {
113
114
const request: FetchOptions = {
115
headers: options.headers,
116
body: options.body,
117
signal: fetchAbortCtl.signal,
118
method: 'POST',
119
callSite: 'nes-completions',
120
};
121
122
const response = await this.fetcherService.fetch(url, request);
123
124
if (response.status === 200 && this.authService.copilotToken?.isFreeUser && this.authService.copilotToken?.isChatQuotaExceeded) {
125
this.authService.resetCopilotToken();
126
}
127
128
if (response.status !== 200) {
129
if (response.status === 402) {
130
// When we receive a 402, we have exceed the free tier quota
131
// This is stored on the token so let's refresh it
132
if (!this.authService.copilotToken?.isCompletionsQuotaExceeded) {
133
this.authService.resetCopilotToken(response.status);
134
await this.authService.getCopilotToken();
135
}
136
}
137
138
return Result.error(new Completions.UnsuccessfulResponse(response.status, response.statusText, response.headers, () => response.text().catch(() => '')));
139
}
140
141
const body = response.body.pipeThrough(new TextDecoderStream());
142
143
const responseStream = streamWithCleanup(body, onCancellationDisposable);
144
145
return Result.ok({
146
status: response.status,
147
statusText: response.statusText,
148
headers: response.headers,
149
body: responseStream,
150
requestId: getRequestId(response.headers),
151
response,
152
});
153
154
} catch (reason: unknown) {
155
156
onCancellationDisposable.dispose();
157
158
if (reason instanceof Error && reason.message === 'This operation was aborted') {
159
return Result.error(new Completions.RequestCancelled());
160
}
161
162
const error = ErrorUtils.fromUnknown(reason);
163
return Result.error(new Completions.Unexpected(error));
164
}
165
}
166
167
private _logCompletionsRequest(
168
url: string,
169
params: IFetchRequestParams,
170
requestId: string,
171
startTimeMs: number,
172
result: Result<ResponseStream, Completions.CompletionsFetchFailure>,
173
): void {
174
if (result.isOk()) {
175
// For successful requests, wait for the stream to complete so we can log the response
176
const responseStream = result.val;
177
void responseStream.response.then(aggregated => {
178
const aggregationStatus = aggregated.isOk() ? 'success' : 'failed';
179
this._emitCompletionsLogEntry(url, params, requestId, startTimeMs, aggregationStatus, aggregated);
180
});
181
} else {
182
const err = result.err;
183
if (err instanceof Completions.RequestCancelled) {
184
this._emitCompletionsLogEntry(url, params, requestId, startTimeMs, 'cancelled', undefined);
185
} else if (err instanceof Completions.UnsuccessfulResponse) {
186
this._emitCompletionsLogEntry(url, params, requestId, startTimeMs, 'failed', undefined, `${err.status} ${err.statusText}`);
187
} else if (err instanceof Completions.Unexpected) {
188
this._emitCompletionsLogEntry(url, params, requestId, startTimeMs, 'failed', undefined, err.error.message);
189
}
190
}
191
}
192
193
private _emitCompletionsLogEntry(
194
url: string,
195
params: IFetchRequestParams,
196
requestId: string,
197
startTimeMs: number,
198
status: 'success' | 'cancelled' | 'failed',
199
aggregatedResponse: Result<Completion, Error> | undefined,
200
errorReason?: string,
201
): void {
202
const durationMs = Date.now() - startTimeMs;
203
const lines: string[] = [];
204
205
lines.push(`> 🚨 Note: This log may contain personal information such as the contents of your files. Please review the contents carefully before sharing.`);
206
lines.push(`# completions`);
207
lines.push(``);
208
209
// Table of contents
210
lines.push(`- [Metadata](#metadata)`);
211
lines.push(`- [Prompt](#prompt)`);
212
if (params.suffix) {
213
lines.push(`- [Suffix](#suffix)`);
214
}
215
lines.push(`- [Response](#response)`);
216
lines.push(``);
217
218
// Metadata
219
lines.push(`## Metadata`);
220
lines.push(`<pre><code>`);
221
lines.push(`url : ${url}`);
222
lines.push(`requestId : ${requestId}`);
223
lines.push(`model : ${params.model ?? '(default)'}`);
224
lines.push(`maxTokens : ${params.max_tokens}`);
225
lines.push(`temperature : ${params.temperature}`);
226
lines.push(`top_p : ${params.top_p}`);
227
lines.push(`n : ${params.n}`);
228
lines.push(`duration : ${durationMs}ms`);
229
lines.push(`</code></pre>`);
230
231
// Prompt
232
lines.push(``);
233
lines.push(`## Prompt`);
234
lines.push(`~~~`);
235
lines.push(params.prompt);
236
lines.push(`~~~`);
237
238
// Suffix
239
if (params.suffix) {
240
lines.push(``);
241
lines.push(`## Suffix`);
242
lines.push(`~~~`);
243
lines.push(params.suffix);
244
lines.push(`~~~`);
245
}
246
247
// Response
248
lines.push(``);
249
lines.push(`## Response`);
250
if (status === 'cancelled') {
251
lines.push(`## CANCELED`);
252
} else if (status === 'failed') {
253
lines.push(`## FAILED: ${errorReason}`);
254
} else if (aggregatedResponse) {
255
if (aggregatedResponse.isOk()) {
256
const completion = aggregatedResponse.val;
257
const text = completion.choices[0]?.text ?? '';
258
const finishReason = completion.choices[0]?.finish_reason ?? 'unknown';
259
lines.push(`~~~`);
260
lines.push(text || '<EMPTY RESPONSE>');
261
lines.push(`~~~`);
262
lines.push(``);
263
lines.push(`<pre><code>`);
264
lines.push(`finishReason : ${finishReason}`);
265
if (completion.usage) {
266
lines.push(`promptTokens : ${completion.usage.prompt_tokens}`);
267
lines.push(`completionTokens : ${completion.usage.completion_tokens}`);
268
lines.push(`totalTokens : ${completion.usage.total_tokens}`);
269
}
270
lines.push(`</code></pre>`);
271
} else {
272
lines.push(`## FAILED: stream error - ${aggregatedResponse.err.message}`);
273
}
274
}
275
276
const icon: ThemeIcon | undefined = status === 'success' ? undefined : Codicon.error;
277
278
this.requestLogger.addEntry({
279
type: LoggedRequestKind.MarkdownContentRequest,
280
debugName: 'Completions Request',
281
startTimeMs,
282
icon,
283
markdownContent: lines.join('\n'),
284
});
285
}
286
287
private getHeaders(
288
requestId: string,
289
secretKey: string,
290
headerOverrides: Record<string, string> = {},
291
): Record<string, string> {
292
const headers: Record<string, string> = {
293
'Content-Type': 'application/json',
294
'x-policy-id': 'nil',
295
Authorization: 'Bearer ' + secretKey,
296
'X-Request-Id': requestId,
297
'X-GitHub-Api-Version': '2025-04-01',
298
...headerOverrides,
299
};
300
301
return headers;
302
}
303
}
304
305
/**
306
* Wraps an async iterable stream and disposes the cleanup disposable when the stream completes or errors.
307
*/
308
async function* streamWithCleanup(
309
stream: AsyncIterable<string>,
310
cleanupDisposable: IDisposable
311
): AsyncGenerator<string> {
312
try {
313
for await (const str of stream) {
314
yield str;
315
}
316
} catch (err: unknown) {
317
const error = ErrorUtils.fromUnknown(err);
318
throw error;
319
} finally {
320
cleanupDisposable.dispose();
321
}
322
}
323
324
/**
325
* Collects all strings from an async iterable and joins them into a single string.
326
*/
327
async function collectAsyncIterableToString(iterable: AsyncIterable<string>): Promise<string> {
328
const parts: string[] = [];
329
for await (const part of iterable) {
330
parts.push(part);
331
}
332
return parts.join('');
333
}
334
335