"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.CompletionsFromNetwork = exports.logger = void 0;
exports.postProcessChoices = postProcessChoices;
exports.makeGhostAPIChoice = makeGhostAPIChoice;
exports.telemetryPerformance = telemetryPerformance;
const statelessNextEditProvider_1 = require("../../../../../../platform/inlineEdits/common/statelessNextEditProvider");
const errors_1 = require("../../../../../../util/common/errors");
const result_1 = require("../../../../../../util/common/result");
const assert_1 = require("../../../../../../util/vs/base/common/assert");
const abstractText_1 = require("../../../../../../util/vs/editor/common/core/text/abstractText");
const instantiation_1 = require("../../../../../../util/vs/platform/instantiation/common/instantiation");
const config_1 = require("../config");
const userErrorNotifier_1 = require("../error/userErrorNotifier");
const featuresService_1 = require("../experiments/featuresService");
const logger_1 = require("../logger");
const networkingTypes_1 = require("../networkingTypes");
const fetch_1 = require("../openai/fetch");
const openai_1 = require("../openai/openai");
const telemetry_1 = require("../telemetry");
const runtimeMode_1 = require("../util/runtimeMode");
const blockTrimmer_1 = require("./blockTrimmer");
const cacheUtils_1 = require("./cacheUtils");
const completionsCache_1 = require("./completionsCache");
const resultType_1 = require("./resultType");
const telemetry_2 = require("./telemetry");
exports.logger = new logger_1.Logger('ghostText');
let CompletionsFromNetwork = class CompletionsFromNetwork {
    constructor(instantiationService, fetcherService, featuresService, runtimeMode, logTarget, completionsCacheService, userErrorNotifier) {
        this.instantiationService = instantiationService;
        this.fetcherService = fetcherService;
        this.featuresService = featuresService;
        this.runtimeMode = runtimeMode;
        this.logTarget = logTarget;
        this.completionsCacheService = completionsCacheService;
        this.userErrorNotifier = userErrorNotifier;
    }
    /** Requests new completion from OpenAI, should be called if and only if the completions for given prompt were not cached before.
     *  It returns only first completion, additional completions are added to the caches in the background.
     *  Copies from the base telemetry data are used as the basis for each choice's telemetry.
     */
    async getCompletionsFromNetwork(requestContext, baseTelemetryData, cancellationToken, finishedCb, telemetryBuilder) {
        return this.genericGetCompletionsFromNetwork(requestContext, baseTelemetryData, cancellationToken, finishedCb, telemetryBuilder, 'completions', async (requestStart, processingTime, choicesStream) => {
            const choicesIterator = choicesStream[Symbol.asyncIterator]();
            const firstRes = await choicesIterator.next();
            if (firstRes.done) {
                exports.logger.debug(this.logTarget, 'All choices redacted');
                return {
                    type: 'empty',
                    reason: 'all choices redacted',
                    telemetryData: (0, telemetry_2.mkBasicResultTelemetry)(baseTelemetryData),
                };
            }
            if (cancellationToken?.isCancellationRequested) {
                exports.logger.debug(this.logTarget, 'Cancelled after awaiting redactedChoices iterator');
                return {
                    type: 'canceled',
                    reason: 'after awaiting redactedChoices iterator',
                    telemetryData: (0, telemetry_2.mkCanceledResultTelemetry)(baseTelemetryData),
                };
            }
            const firstChoice = firstRes.value;
            if (firstChoice === undefined) {
                // This is probably unreachable given the firstRes.done check above
                exports.logger.debug(this.logTarget, 'Got undefined choice from redactedChoices iterator');
                return {
                    type: 'empty',
                    reason: 'got undefined choice from redactedChoices iterator',
                    telemetryData: (0, telemetry_2.mkBasicResultTelemetry)(baseTelemetryData),
                };
            }
            this.instantiationService.invokeFunction(telemetryPerformance, 'performance', firstChoice, requestStart, processingTime);
            exports.logger.debug(this.logTarget, `Awaited first result, id:  ${firstChoice.choiceIndex}`);
            // Adds first result to cache
            const processedFirstChoice = postProcessChoices(firstChoice);
            if (processedFirstChoice) {
                (0, cacheUtils_1.appendToCache)(this.completionsCacheService, requestContext, processedFirstChoice);
                exports.logger.debug(this.logTarget, `GhostText first completion (index ${processedFirstChoice?.choiceIndex}): ${JSON.stringify(processedFirstChoice?.completionText)}`);
            }
            //Create promise for each result, don't `await` it (unless in test mode) but handle asynchronously with `.then()`
            const cacheDone = (async () => {
                const apiChoices = processedFirstChoice !== undefined ? [processedFirstChoice] : [];
                for await (const choice of choicesStream) {
                    if (choice === undefined) {
                        continue;
                    }
                    exports.logger.debug(this.logTarget, `GhostText later completion (index ${choice?.choiceIndex}): ${JSON.stringify(choice.completionText)}`);
                    const processedChoice = postProcessChoices(choice, apiChoices);
                    if (!processedChoice) {
                        continue;
                    }
                    apiChoices.push(processedChoice);
                    (0, cacheUtils_1.appendToCache)(this.completionsCacheService, requestContext, processedChoice);
                }
            })();
            if (this.runtimeMode.isRunningInTest()) {
                await cacheDone;
            }
            if (processedFirstChoice) {
                // Because we ask the server to stop at \n above, we don't need to force single line here
                return {
                    type: 'success',
                    value: [makeGhostAPIChoice(processedFirstChoice, { forceSingleLine: false }), cacheDone],
                    telemetryData: (0, telemetry_2.mkBasicResultTelemetry)(baseTelemetryData),
                    telemetryBlob: baseTelemetryData,
                    resultType: resultType_1.ResultType.Network,
                };
            }
            else {
                return {
                    type: 'empty',
                    reason: 'got undefined processedFirstChoice',
                    telemetryData: (0, telemetry_2.mkBasicResultTelemetry)(baseTelemetryData),
                };
            }
        });
    }
    /** Requests new completion from OpenAI, should be called if and only if we are in the servers-side termination mode, and it's follow-up cycling request
     *  It returns all requested completions
     *  Copies from the base telemetry data are used as the basis for each choice's telemetry.
     */
    async getAllCompletionsFromNetwork(requestContext, baseTelemetryData, cancellationToken, finishedCb, telemetryBuilder) {
        return this.genericGetCompletionsFromNetwork(requestContext, baseTelemetryData, cancellationToken, finishedCb, telemetryBuilder, 'all completions', async (requestStart, processingTime, choicesStream) => {
            const apiChoices = [];
            for await (const choice of choicesStream) {
                if (cancellationToken?.isCancellationRequested) {
                    exports.logger.debug(this.logTarget, 'Cancelled after awaiting choices iterator');
                    return {
                        type: 'canceled',
                        reason: 'after awaiting choices iterator',
                        telemetryData: (0, telemetry_2.mkCanceledResultTelemetry)(baseTelemetryData),
                    };
                }
                const processedChoice = postProcessChoices(choice, apiChoices);
                if (!processedChoice) {
                    continue;
                }
                apiChoices.push(processedChoice);
            }
            //Append results to current completions cache, and network cache
            if (apiChoices.length > 0) {
                for (const choice of apiChoices) {
                    (0, cacheUtils_1.appendToCache)(this.completionsCacheService, requestContext, choice);
                }
                this.instantiationService.invokeFunction(telemetryPerformance, 'cyclingPerformance', apiChoices[0], requestStart, processingTime);
            }
            return {
                type: 'success',
                value: [apiChoices, Promise.resolve()],
                telemetryData: (0, telemetry_2.mkBasicResultTelemetry)(baseTelemetryData),
                telemetryBlob: baseTelemetryData,
                resultType: resultType_1.ResultType.Cycling,
            };
        });
    }
    async genericGetCompletionsFromNetwork(requestContext, baseTelemetryData, cancellationToken, finishedCb, telemetryBuilder, what, processChoices) {
        const statelessTelemetryBuilder = new statelessNextEditProvider_1.StatelessNextEditTelemetryBuilder(requestContext.ourRequestId);
        const result = await this._genericGetCompletionsFromNetwork(requestContext, baseTelemetryData, cancellationToken, finishedCb, statelessTelemetryBuilder, what, processChoices);
        let editResult;
        switch (result.type) {
            case 'success':
                editResult = result_1.Result.ok(undefined);
                break;
            case 'canceled':
                editResult = result_1.Result.error(new statelessNextEditProvider_1.NoNextEditReason.GotCancelled(result.reason));
                break;
            case 'empty':
                editResult = result_1.Result.error(new statelessNextEditProvider_1.NoNextEditReason.NoSuggestions(new abstractText_1.StringText('') /* unused by completions anyway */, undefined));
                break;
            case 'failed':
                editResult = result_1.Result.error(new statelessNextEditProvider_1.NoNextEditReason.Uncategorized(errors_1.ErrorUtils.fromUnknown(result.reason)));
                break;
            case 'abortedBeforeIssued':
            case 'promptOnly':
                editResult = result_1.Result.error(new statelessNextEditProvider_1.NoNextEditReason.GotCancelled(result.reason));
                break;
            default:
                (0, assert_1.assertNever)(result);
        }
        telemetryBuilder.setStatelessNextEditTelemetry(statelessTelemetryBuilder.build(editResult));
        return result;
    }
    async _genericGetCompletionsFromNetwork(requestContext, baseTelemetryData, cancellationToken, finishedCb, telemetryBuilder, what, processChoices) {
        exports.logger.debug(this.logTarget, `Getting ${what} from network`);
        // copy the base telemetry data
        baseTelemetryData = baseTelemetryData.extendedBy();
        telemetryBuilder.setModelName(requestContext.engineModelId);
        // Request one choice for automatic requests, three for invoked (cycling) requests.
        const n = requestContext.isCycling ? 3 : 1;
        const temperature = (0, openai_1.getTemperatureForSamples)(this.runtimeMode, n);
        const extra = {
            language: requestContext.languageId,
            next_indent: requestContext.indentation.next ?? 0,
            trim_by_indentation: (0, config_1.shouldDoServerTrimming)(requestContext.blockMode),
            prompt_tokens: requestContext.prompt.prefixTokens ?? 0,
            suffix_tokens: requestContext.prompt.suffixTokens ?? 0,
        };
        const postOptions = { n, temperature, code_annotations: false };
        const modelTerminatesSingleline = this.featuresService.modelAlwaysTerminatesSingleline(baseTelemetryData);
        const simulateSingleline = requestContext.blockMode === config_1.BlockMode.MoreMultiline &&
            blockTrimmer_1.BlockTrimmer.isSupported(requestContext.languageId) &&
            !modelTerminatesSingleline;
        if (!requestContext.multiline && !simulateSingleline) {
            // If we are not in multiline mode, we get the server to truncate the results. This does mean that we
            // also cache a single line result which will be reused even if we are later in multiline mode. This is
            // an acceptable trade-off as the transition should be relatively rare and truncating on the server is
            // more efficient.
            // Note that this also means we don't need to truncate when creating the GhostAPIChoice object below.
            postOptions['stop'] = ['\n'];
        }
        else if (requestContext.stop) {
            postOptions['stop'] = requestContext.stop;
        }
        if (requestContext.maxTokens !== undefined) {
            postOptions['max_tokens'] = requestContext.maxTokens;
        }
        const requestStart = Date.now();
        // extend telemetry data
        const newProperties = {
            endpoint: 'completions',
            uiKind: fetch_1.CopilotUiKind.GhostText,
            temperature: JSON.stringify(temperature),
            n: JSON.stringify(n),
            stop: JSON.stringify(postOptions['stop']) ?? 'unset',
            logit_bias: JSON.stringify(null),
        };
        Object.assign(baseTelemetryData.properties, newProperties);
        try {
            const completionParams = {
                prompt: requestContext.prompt,
                languageId: requestContext.languageId,
                repoInfo: requestContext.repoInfo,
                ourRequestId: requestContext.ourRequestId,
                engineModelId: requestContext.engineModelId,
                count: n,
                uiKind: fetch_1.CopilotUiKind.GhostText,
                postOptions,
                headers: requestContext.headers,
                extra,
            };
            const res = await this.fetcherService.fetchAndStreamCompletions(completionParams, baseTelemetryData, finishedCb, cancellationToken);
            if (res.type === 'failed') {
                return {
                    type: 'failed',
                    reason: res.reason,
                    telemetryData: (0, telemetry_2.mkBasicResultTelemetry)(baseTelemetryData),
                };
            }
            if (res.type === 'canceled') {
                exports.logger.debug(this.logTarget, 'Cancelled after awaiting fetchCompletions');
                return {
                    type: 'canceled',
                    reason: res.reason,
                    telemetryData: (0, telemetry_2.mkCanceledResultTelemetry)(baseTelemetryData),
                };
            }
            return processChoices(requestStart, res.getProcessingTime(), res.choices);
        }
        catch (err) {
            // If we cancelled a network request, we don't want to log an error
            if ((0, networkingTypes_1.isAbortError)(err)) {
                return {
                    type: 'canceled',
                    reason: 'network request aborted',
                    telemetryData: (0, telemetry_2.mkCanceledResultTelemetry)(baseTelemetryData, {
                        cancelledNetworkRequest: true,
                    }),
                };
            }
            else {
                this.instantiationService.invokeFunction(acc => exports.logger.exception(acc, err, `Error on ghost text request`));
                this.userErrorNotifier.notifyUser(err);
                if (this.runtimeMode.shouldFailForDebugPurposes()) {
                    throw err;
                }
                // not including err in this result because it'll end up in standard telemetry
                return {
                    type: 'failed',
                    reason: 'non-abort error on ghost text request',
                    telemetryData: (0, telemetry_2.mkBasicResultTelemetry)(baseTelemetryData),
                };
            }
        }
    }
};
exports.CompletionsFromNetwork = CompletionsFromNetwork;
exports.CompletionsFromNetwork = CompletionsFromNetwork = __decorate([
    __param(0, instantiation_1.IInstantiationService),
    __param(1, fetch_1.ICompletionsOpenAIFetcherService),
    __param(2, featuresService_1.ICompletionsFeaturesService),
    __param(3, runtimeMode_1.ICompletionsRuntimeModeService),
    __param(4, logger_1.ICompletionsLogTargetService),
    __param(5, completionsCache_1.ICompletionsCacheService),
    __param(6, userErrorNotifier_1.ICompletionsUserErrorNotifierService)
], CompletionsFromNetwork);
/**
 * Post-proceses a completion choice based on the current request context and existing choices.
 */
function postProcessChoices(newChoice, currentChoices) {
    if (!currentChoices) {
        currentChoices = [];
    }
    newChoice.completionText = newChoice.completionText.trimEnd();
    if (!newChoice.completionText) {
        return undefined;
    }
    // Collect only unique displayTexts
    if (currentChoices.findIndex(v => v.completionText.trim() === newChoice.completionText.trim()) !== -1) {
        return undefined;
    }
    return newChoice;
}
function makeGhostAPIChoice(choice, options) {
    const ghostChoice = { ...choice };
    if (options.forceSingleLine) {
        const { completionText } = ghostChoice;
        // Special case for when completion starts with a newline, don't count that as its own line
        const initialLineBreak = completionText.match(/^\r?\n/);
        if (initialLineBreak) {
            ghostChoice.completionText = initialLineBreak[0] + completionText.split('\n')[1];
        }
        else {
            ghostChoice.completionText = completionText.split('\n')[0];
        }
    }
    return ghostChoice;
}
function telemetryPerformance(accessor, performanceKind, choice, requestStart, processingTimeMs) {
    const requestTimeMs = Date.now() - requestStart;
    const deltaMs = requestTimeMs - processingTimeMs;
    const telemetryData = choice.telemetryData.extendedBy({}, {
        completionCharLen: choice.completionText.length,
        requestTimeMs: requestTimeMs,
        processingTimeMs: processingTimeMs,
        deltaMs: deltaMs,
        // Choice properties
        meanLogProb: choice.meanLogProb || NaN,
        meanAlternativeLogProb: choice.meanAlternativeLogProb || NaN,
    });
    telemetryData.extendWithRequestId(choice.requestId);
    (0, telemetry_1.telemetry)(accessor, `ghostText.${performanceKind}`, telemetryData);
}
//# sourceMappingURL=completionsFromNetwork.js.map