"use strict";
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CompletionsFetchService = void 0;
const asyncIterableUtils_1 = require("../../../util/common/asyncIterableUtils");
const errors_1 = require("../../../util/common/errors");
const result_1 = require("../../../util/common/result");
const codicons_1 = require("../../../util/vs/base/common/codicons");
const authentication_1 = require("../../authentication/common/authentication");
const fetch_1 = require("../../networking/common/fetch");
const fetcherService_1 = require("../../networking/common/fetcherService");
const requestLogger_1 = require("../../requestLogger/common/requestLogger");
const completionsFetchService_1 = require("../common/completionsFetchService");
const responseStream_1 = require("../common/responseStream");
const streamTransformer_1 = require("./streamTransformer");
let CompletionsFetchService = class CompletionsFetchService {
    constructor(authService, fetcherService, requestLogger) {
        this.authService = authService;
        this.fetcherService = fetcherService;
        this.requestLogger = requestLogger;
    }
    disconnectAll() {
        return this.fetcherService.disconnectAll();
    }
    async fetch(url, secretKey, params, requestId, ct, headerOverrides) {
        const startTimeMs = Date.now();
        if (ct.isCancellationRequested) {
            const result = result_1.Result.error(new completionsFetchService_1.Completions.RequestCancelled());
            this._logCompletionsRequest(url, params, requestId, startTimeMs, result);
            return result;
        }
        const options = {
            requestId,
            headers: this.getHeaders(requestId, secretKey, headerOverrides),
            body: JSON.stringify({
                ...params,
                stream: true,
            })
        };
        const fetchResponse = await this._fetchFromUrl(url, options, ct);
        if (fetchResponse.isError()) {
            this._logCompletionsRequest(url, params, requestId, startTimeMs, fetchResponse);
            return fetchResponse;
        }
        if (fetchResponse.val.status === 200) {
            const jsonlStream = asyncIterableUtils_1.AsyncIterUtilsExt.splitLines(fetchResponse.val.body);
            const completionsStream = (0, streamTransformer_1.jsonlStreamToCompletions)(jsonlStream);
            const response = new responseStream_1.ResponseStream(fetchResponse.val.response, completionsStream, fetchResponse.val.requestId, fetchResponse.val.headers);
            const result = result_1.Result.ok(response);
            this._logCompletionsRequest(url, params, requestId, startTimeMs, result);
            return result;
        }
        else {
            const error = new completionsFetchService_1.Completions.UnsuccessfulResponse(fetchResponse.val.status, fetchResponse.val.statusText, fetchResponse.val.headers, () => collectAsyncIterableToString(fetchResponse.val.body).catch(() => ''));
            const result = result_1.Result.error(error);
            this._logCompletionsRequest(url, params, requestId, startTimeMs, result);
            return result;
        }
    }
    async _fetchFromUrl(url, options, ct) {
        const fetchAbortCtl = this.fetcherService.makeAbortController();
        const onCancellationDisposable = ct.onCancellationRequested(() => {
            fetchAbortCtl.abort();
        });
        try {
            const request = {
                headers: options.headers,
                body: options.body,
                signal: fetchAbortCtl.signal,
                method: 'POST',
                callSite: 'nes-completions',
            };
            const response = await this.fetcherService.fetch(url, request);
            if (response.status === 200 && this.authService.copilotToken?.isFreeUser && this.authService.copilotToken?.isChatQuotaExceeded) {
                this.authService.resetCopilotToken();
            }
            if (response.status !== 200) {
                if (response.status === 402) {
                    // When we receive a 402, we have exceed the free tier quota
                    // This is stored on the token so let's refresh it
                    if (!this.authService.copilotToken?.isCompletionsQuotaExceeded) {
                        this.authService.resetCopilotToken(response.status);
                        await this.authService.getCopilotToken();
                    }
                }
                return result_1.Result.error(new completionsFetchService_1.Completions.UnsuccessfulResponse(response.status, response.statusText, response.headers, () => response.text().catch(() => '')));
            }
            const body = response.body.pipeThrough(new TextDecoderStream());
            const responseStream = streamWithCleanup(body, onCancellationDisposable);
            return result_1.Result.ok({
                status: response.status,
                statusText: response.statusText,
                headers: response.headers,
                body: responseStream,
                requestId: (0, fetch_1.getRequestId)(response.headers),
                response,
            });
        }
        catch (reason) {
            onCancellationDisposable.dispose();
            if (reason instanceof Error && reason.message === 'This operation was aborted') {
                return result_1.Result.error(new completionsFetchService_1.Completions.RequestCancelled());
            }
            const error = errors_1.ErrorUtils.fromUnknown(reason);
            return result_1.Result.error(new completionsFetchService_1.Completions.Unexpected(error));
        }
    }
    _logCompletionsRequest(url, params, requestId, startTimeMs, result) {
        if (result.isOk()) {
            // For successful requests, wait for the stream to complete so we can log the response
            const responseStream = result.val;
            void responseStream.response.then(aggregated => {
                const aggregationStatus = aggregated.isOk() ? 'success' : 'failed';
                this._emitCompletionsLogEntry(url, params, requestId, startTimeMs, aggregationStatus, aggregated);
            });
        }
        else {
            const err = result.err;
            if (err instanceof completionsFetchService_1.Completions.RequestCancelled) {
                this._emitCompletionsLogEntry(url, params, requestId, startTimeMs, 'cancelled', undefined);
            }
            else if (err instanceof completionsFetchService_1.Completions.UnsuccessfulResponse) {
                this._emitCompletionsLogEntry(url, params, requestId, startTimeMs, 'failed', undefined, `${err.status} ${err.statusText}`);
            }
            else if (err instanceof completionsFetchService_1.Completions.Unexpected) {
                this._emitCompletionsLogEntry(url, params, requestId, startTimeMs, 'failed', undefined, err.error.message);
            }
        }
    }
    _emitCompletionsLogEntry(url, params, requestId, startTimeMs, status, aggregatedResponse, errorReason) {
        const durationMs = Date.now() - startTimeMs;
        const lines = [];
        lines.push(`> 🚨 Note: This log may contain personal information such as the contents of your files. Please review the contents carefully before sharing.`);
        lines.push(`# completions`);
        lines.push(``);
        // Table of contents
        lines.push(`- [Metadata](#metadata)`);
        lines.push(`- [Prompt](#prompt)`);
        if (params.suffix) {
            lines.push(`- [Suffix](#suffix)`);
        }
        lines.push(`- [Response](#response)`);
        lines.push(``);
        // Metadata
        lines.push(`## Metadata`);
        lines.push(`<pre><code>`);
        lines.push(`url              : ${url}`);
        lines.push(`requestId        : ${requestId}`);
        lines.push(`model            : ${params.model ?? '(default)'}`);
        lines.push(`maxTokens        : ${params.max_tokens}`);
        lines.push(`temperature      : ${params.temperature}`);
        lines.push(`top_p            : ${params.top_p}`);
        lines.push(`n                : ${params.n}`);
        lines.push(`duration         : ${durationMs}ms`);
        lines.push(`</code></pre>`);
        // Prompt
        lines.push(``);
        lines.push(`## Prompt`);
        lines.push(`~~~`);
        lines.push(params.prompt);
        lines.push(`~~~`);
        // Suffix
        if (params.suffix) {
            lines.push(``);
            lines.push(`## Suffix`);
            lines.push(`~~~`);
            lines.push(params.suffix);
            lines.push(`~~~`);
        }
        // Response
        lines.push(``);
        lines.push(`## Response`);
        if (status === 'cancelled') {
            lines.push(`## CANCELED`);
        }
        else if (status === 'failed') {
            lines.push(`## FAILED: ${errorReason}`);
        }
        else if (aggregatedResponse) {
            if (aggregatedResponse.isOk()) {
                const completion = aggregatedResponse.val;
                const text = completion.choices[0]?.text ?? '';
                const finishReason = completion.choices[0]?.finish_reason ?? 'unknown';
                lines.push(`~~~`);
                lines.push(text || '<EMPTY RESPONSE>');
                lines.push(`~~~`);
                lines.push(``);
                lines.push(`<pre><code>`);
                lines.push(`finishReason     : ${finishReason}`);
                if (completion.usage) {
                    lines.push(`promptTokens     : ${completion.usage.prompt_tokens}`);
                    lines.push(`completionTokens : ${completion.usage.completion_tokens}`);
                    lines.push(`totalTokens      : ${completion.usage.total_tokens}`);
                }
                lines.push(`</code></pre>`);
            }
            else {
                lines.push(`## FAILED: stream error - ${aggregatedResponse.err.message}`);
            }
        }
        const icon = status === 'success' ? undefined : codicons_1.Codicon.error;
        this.requestLogger.addEntry({
            type: "MarkdownContentRequest" /* LoggedRequestKind.MarkdownContentRequest */,
            debugName: 'Completions Request',
            startTimeMs,
            icon,
            markdownContent: lines.join('\n'),
        });
    }
    getHeaders(requestId, secretKey, headerOverrides = {}) {
        const headers = {
            'Content-Type': 'application/json',
            'x-policy-id': 'nil',
            Authorization: 'Bearer ' + secretKey,
            'X-Request-Id': requestId,
            'X-GitHub-Api-Version': '2025-04-01',
            ...headerOverrides,
        };
        return headers;
    }
};
exports.CompletionsFetchService = CompletionsFetchService;
exports.CompletionsFetchService = CompletionsFetchService = __decorate([
    __param(0, authentication_1.IAuthenticationService),
    __param(1, fetcherService_1.IFetcherService),
    __param(2, requestLogger_1.IRequestLogger)
], CompletionsFetchService);
/**
 * Wraps an async iterable stream and disposes the cleanup disposable when the stream completes or errors.
 */
async function* streamWithCleanup(stream, cleanupDisposable) {
    try {
        for await (const str of stream) {
            yield str;
        }
    }
    catch (err) {
        const error = errors_1.ErrorUtils.fromUnknown(err);
        throw error;
    }
    finally {
        cleanupDisposable.dispose();
    }
}
/**
 * Collects all strings from an async iterable and joins them into a single string.
 */
async function collectAsyncIterableToString(iterable) {
    const parts = [];
    for await (const part of iterable) {
        parts.push(part);
    }
    return parts.join('');
}
//# sourceMappingURL=completionsFetchServiceImpl.js.map