@helicone/helpers
Version:
A Node.js wrapper for some of Helicone's common functionalities
334 lines (305 loc) • 9.57 kB
text/typescript
import {
IHeliconeManualLogger,
HeliconeLogRequest,
ProviderRequest,
ProviderResponse,
Timing,
} from "./types";
import { HeliconeLogBuilder } from "./HeliconeLogBuilder";
export { HeliconeLogBuilder };
export class HeliconeManualLogger {
private apiKey: string;
private headers: Record<string, string>;
private LOGGING_ENDPOINT: string =
"https://api.worker.helicone.ai/custom/v1/log";
constructor(opts: IHeliconeManualLogger) {
this.apiKey = opts.apiKey;
this.headers = opts.headers || {};
this.LOGGING_ENDPOINT = opts.loggingEndpoint || this.LOGGING_ENDPOINT;
}
/**
* Creates a log builder for more flexible stream handling with error management
* @param request - The request object to log
* @param additionalHeaders - Additional headers to send with the request
* @returns A HeliconeLogBuilder instance
*/
public logBuilder(
request: HeliconeLogRequest,
additionalHeaders?: Record<string, string>
): HeliconeLogBuilder {
return new HeliconeLogBuilder(this, request, additionalHeaders);
}
/**
* Logs a custom request to Helicone
* @param request - The request object to log
* @param operation - The operation which will be executed and logged
* @param additionalHeaders - Additional headers to send with the request
* @returns The result of the `operation` function
*/
public async logRequest<T>(
request: HeliconeLogRequest,
operation: (resultRecorder: HeliconeResultRecorder) => Promise<T>,
additionalHeaders?: Record<string, string>
): Promise<T> {
const startTime = Date.now();
const resultRecorder = new HeliconeResultRecorder();
try {
const result = await operation(resultRecorder);
const endTime = Date.now();
await this.sendLog(request, resultRecorder.getResults(), {
startTime,
endTime,
additionalHeaders,
status: 200,
});
return result;
} catch (error) {
console.error("Error during operation:", error);
throw error;
}
}
/**
* Logs a single stream to Helicone
* @param request - The request object to log
* @param stream - The ReadableStream to consume and log
* @param additionalHeaders - Additional headers to send with the request
* @returns A Promise that resolves when logging is complete
*/
public async logSingleStream(
request: HeliconeLogRequest,
stream: ReadableStream,
additionalHeaders?: Record<string, string>
): Promise<void> {
const startTime = Date.now();
const resultRecorder = new HeliconeStreamResultRecorder();
resultRecorder.attachStream(stream);
let firstChunkTimeUnix: number | null = null;
const streamedData: any[] = [];
const decoder = new TextDecoder();
for await (const chunk of stream) {
if (!firstChunkTimeUnix) {
firstChunkTimeUnix = Date.now();
}
streamedData.push(decoder.decode(chunk));
}
await this.sendLog(request, streamedData.join(""), {
startTime,
endTime: Date.now(),
additionalHeaders,
timeToFirstToken: firstChunkTimeUnix
? firstChunkTimeUnix - startTime
: undefined,
status: 200,
});
}
/**
* Logs a single request with a response body to Helicone
* @param request - The request object to log
* @param body - The response body as a string
* @param additionalHeaders - Additional headers to send with the request
* @param latencyMs - The latency of the request in milliseconds
* @returns A Promise that resolves when logging is complete
*
* @example
* ```typescript
* helicone.logSingleRequest(request, body, { additionalHeaders: { "Helicone-User-Id": userId }, latencyMs: 1000 });
* ```
*/
public async logSingleRequest(
request: HeliconeLogRequest,
body: string,
options: {
additionalHeaders?: Record<string, string>;
latencyMs?: number;
}
): Promise<void> {
const startTime = Date.now();
const endTime = options.latencyMs
? startTime + options.latencyMs
: Date.now();
await this.sendLog(request, body, {
startTime,
endTime,
additionalHeaders: options.additionalHeaders,
status: 200,
});
}
/**
* Logs a streaming operation to Helicone
* @param request - The request object to log
* @param operation - The operation which will be executed and logged, with access to a stream recorder
* @param additionalHeaders - Additional headers to send with the request
* @returns The result of the `operation` function
*
* @example
* ```typescript
* const response = await llmProvider.createChatCompletion({ stream: true, ... });
* const [stream1, stream2] = response.tee();
*
* helicone.logStream(
* requestBody,
* async (resultRecorder) => {
* resultRecorder.attachStream(stream2.toReadableStream());
* return stream1;
* },
* { "Helicone-User-Id": userId }
* );
* ```
*/
public async logStream<T>(
request: HeliconeLogRequest,
operation: (resultRecorder: HeliconeStreamResultRecorder) => Promise<T>,
additionalHeaders?: Record<string, string>
): Promise<T> {
const startTime = Date.now();
const resultRecorder = new HeliconeStreamResultRecorder();
const result = await operation(resultRecorder);
try {
await resultRecorder.getStreamTexts().then(async (texts) => {
const endTime = Date.now();
await this.sendLog(request, texts.join(""), {
startTime,
endTime,
additionalHeaders,
timeToFirstToken: resultRecorder.firstChunkTimeUnix
? resultRecorder.firstChunkTimeUnix - startTime
: undefined,
status: 200,
});
});
return result;
} catch (error) {
console.error("Helicone error during stream logging:", error);
throw error;
}
}
public async sendLog(
request: HeliconeLogRequest,
response: Record<string, any> | string,
options: {
startTime: number;
endTime: number;
additionalHeaders?: Record<string, string>;
timeToFirstToken?: number;
status?: number;
}
): Promise<void> {
const { startTime, endTime, additionalHeaders, status = 200 } = options;
const providerRequest: ProviderRequest = {
url: "custom-model-nopath",
json: {
...request,
},
meta: {},
};
const isResponseString = typeof response === "string";
const providerResponse: ProviderResponse = {
headers: this.headers,
status: status,
json: isResponseString
? {}
: {
...response,
_type: request._type,
toolName: request.toolName,
},
textBody: isResponseString ? response : undefined,
};
const timing: Timing = {
startTime: {
seconds: Math.trunc(startTime / 1000),
milliseconds: startTime % 1000,
},
endTime: {
seconds: Math.trunc(endTime / 1000),
milliseconds: endTime % 1000,
},
timeToFirstToken: options.timeToFirstToken,
};
const fetchOptions = {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
...this.headers,
...additionalHeaders,
},
body: JSON.stringify({
providerRequest,
providerResponse,
timing,
}),
};
try {
const response = await fetch(this.LOGGING_ENDPOINT, fetchOptions);
if (!response.ok) {
console.error(
"Error making request to Helicone log endpoint:",
response.statusText
);
}
} catch (error: any) {
console.error(
"Error making request to Helicone log endpoint:",
error?.message,
error
);
}
}
}
/**
* Recorder for handling and processing streams in Helicone logging
* Used to capture and process streaming responses from LLM providers
*/
class HeliconeStreamResultRecorder {
private streams: ReadableStream[] = [];
firstChunkTimeUnix: number | null = null;
constructor() {}
/**
* Attaches a ReadableStream to be processed
* @param stream - The ReadableStream to attach
*/
attachStream(stream: ReadableStream): void {
this.streams.push(stream);
}
/**
* Processes all attached streams and returns their contents as strings
* @returns Promise resolving to an array of strings containing the content of each stream
*/
async getStreamTexts(): Promise<string[]> {
const decoder = new TextDecoder();
return Promise.all(
this.streams.map(async (stream) => {
if (!this.firstChunkTimeUnix) {
this.firstChunkTimeUnix = Date.now();
}
const streamedData: any[] = [];
for await (const chunk of stream) {
streamedData.push(decoder.decode(chunk));
}
return streamedData.join("");
})
);
}
}
/**
* Recorder for handling and storing results in Helicone logging
* Used to capture non-streaming responses from operations
*/
class HeliconeResultRecorder {
private results: Record<string, any> = {};
/**
* Appends data to the results object
* @param data - The data to append to the results
*/
appendResults(data: Record<string, any>): void {
this.results = { ...this.results, ...data };
}
/**
* Gets the current results
* @returns The current results object
*/
getResults(): Record<string, any> {
return this.results;
}
}