UNPKG

@helicone/helpers

Version:

A Node.js wrapper for some of Helicone's common functionalities

188 lines 6.83 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.HeliconeLogBuilder = void 0; /** * HeliconeLogBuilder provides a simplified way to handle streaming LLM responses * with better error handling and async support. */ class HeliconeLogBuilder { /** * Creates a new HeliconeLogBuilder * @param logger - The HeliconeManualLogger instance to use for logging * @param request - The request object to log * @param additionalHeaders - Additional headers to send with the request */ constructor(logger, request, additionalHeaders) { this.endTime = 0; this.responseBody = ""; this.error = null; this.streamTexts = []; this.status = 200; this.wasCancelled = false; this.streamState = { isPolling: false, alreadyAttached: false, }; this.attachedStream = null; this.logger = logger; this.request = request; this.additionalHeaders = additionalHeaders; this.startTime = performance.now(); } /** * Sets an error that occurred during the request * @param error - The error that occurred */ setError(error) { this.error = error; this.endTime = performance.now(); this.status = 500; } /** * Collects streaming responses and converts them to a readable stream * while also capturing the response for logging * @param stream - The stream from an LLM provider response * @returns A ReadableStream that can be returned to the client */ toReadableStream(stream) { if (this.streamState.alreadyAttached) { throw new Error("Cannot attach multiple streams"); } this.streamState.alreadyAttached = true; const self = stream; let iter; const encoder = new TextEncoder(); // Store the reference to this for use in the ReadableStream const builder = this; this.streamState.isPolling = true; return new ReadableStream({ async start() { iter = self[Symbol.asyncIterator](); }, async pull(ctrl) { try { const { value, done } = await iter.next(); if (done) { builder.endTime = performance.now(); builder.streamState.isPolling = false; return ctrl.close(); } if (!builder.timeToFirstToken) { builder.timeToFirstToken = performance.now() - builder.startTime; } const json = JSON.stringify(value) + "\n"; builder.streamTexts.push(json); const bytes = encoder.encode(json); ctrl.enqueue(bytes); } catch (err) { builder.error = err; builder.endTime = performance.now(); builder.status = 500; builder.streamState.isPolling = false; ctrl.error(err); } }, async cancel() { builder.wasCancelled = true; builder.endTime = performance.now(); builder.streamState.isPolling = false; await iter.return?.(); }, }); } addAdditionalHeaders(headers) { this.additionalHeaders = { ...this.additionalHeaders, ...headers, }; } /** * Attaches a stream to the log builder, this will consume the stream and log it on sendLog * @param stream - The stream to attach */ async attachStream(stream) { if (this.attachedStream) { throw new Error("Cannot attach multiple streams"); } this.attachedStream = stream; await this.consumeStream(); } /** * Sets the response body for non-streaming responses * @param body - The response body */ setResponse(body) { this.responseBody = body; this.endTime = performance.now(); } async waitForStreamToFinish() { const maxWaitTime = 10000; // 10 seconds const startTime = performance.now(); while (this.streamState.isPolling) { if (performance.now() - startTime > maxWaitTime) { throw new Error("Stream took too long to finish"); } await new Promise((resolve) => setTimeout(resolve, 100)); } return; } async consumeStream() { if (this.attachedStream && !this.streamState.isPolling) { const stream = this.toReadableStream(this.attachedStream); const reader = stream.getReader(); while (true) { const { done } = await reader.read(); if (done) { break; } } } } /** * Sends the log to Helicone * @returns A Promise that resolves when logging is complete */ async sendLog() { await this.waitForStreamToFinish(); if (this.endTime === 0) { this.endTime = performance.now(); } try { if (this.wasCancelled) { this.status = -3; } // Handle normal case let response = this.streamTexts.length > 0 ? this.streamTexts.join("") : this.responseBody; if (this.error && !this.wasCancelled) { response = (this.error instanceof Error ? this.error.stack || this.error.message : String(this.error)) + "\n\n" + response; } // Convert high-resolution time to Unix timestamps for the API const startTimeUnix = Date.now() - Math.round(performance.now() - this.startTime); const endTimeUnix = Date.now() - Math.round(performance.now() - this.endTime); const timeToFirstTokenMs = this.timeToFirstToken ? Math.round(this.timeToFirstToken) : undefined; await this.logger.sendLog(this.request, response, { startTime: startTimeUnix, endTime: endTimeUnix, additionalHeaders: this.additionalHeaders, timeToFirstToken: timeToFirstTokenMs, status: this.status, }); } catch (error) { console.error("Error sending log to Helicone:", error); throw error; } } } exports.HeliconeLogBuilder = HeliconeLogBuilder; //# sourceMappingURL=HeliconeLogBuilder.js.map