@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
141 lines (140 loc) • 5.56 kB
JavaScript
/**
* Telemetry Handler Module
*
* Handles analytics, evaluation and performance metrics.
* Extracted from BaseProvider to follow Single Responsibility Principle.
*
* Responsibilities:
* - Analytics creation and tracking
* - Evaluation generation
* - Performance metrics recording
* - Cost calculation
*
* @module core/modules/TelemetryHandler
*/
import { nanoid } from "nanoid";
import { extractTokenUsage } from "../../utils/tokenUtils.js";
import { logger } from "../../utils/logger.js";
import { recordProviderPerformanceFromMetrics } from "../evaluationProviders.js";
import { calculateCost, hasPricing } from "../../utils/pricing.js";
/**
* TelemetryHandler class - Handles analytics and telemetry for AI providers
*/
export class TelemetryHandler {
providerName;
modelName;
neurolink;
constructor(providerName, modelName, neurolink) {
this.providerName = providerName;
this.modelName = modelName;
this.neurolink = neurolink;
}
/**
* Create analytics for a generation result
*/
async createAnalytics(result, responseTime, context) {
const { createAnalytics } = await import("../analytics.js");
return createAnalytics(this.providerName, this.modelName, result, responseTime, context);
}
/**
* Create evaluation for a generation result
*/
async createEvaluation(result, options) {
const { evaluateResponse } = await import("../evaluation.js");
const context = {
userQuery: options.prompt || options.input?.text || "Generated response",
aiResponse: result.content,
context: options.context,
primaryDomain: options.evaluationDomain,
assistantRole: "AI assistant",
conversationHistory: options.conversationHistory?.map((msg) => ({
role: msg.role,
content: msg.content,
})),
toolUsage: options.toolUsageContext
? [
{
toolName: options.toolUsageContext,
input: {},
output: {},
executionTime: 0,
},
]
: undefined,
expectedOutcome: options.expectedOutcome,
evaluationCriteria: options.evaluationCriteria,
};
const evaluation = await evaluateResponse(context);
return evaluation;
}
/**
* Record performance metrics for a generation
*/
async recordPerformanceMetrics(usage, responseTime) {
try {
// Normalize first: rebases cache-inclusive ai@6 input onto the uncached
// remainder and surfaces cache tiers so the cost is priced per tier
// instead of billing cache reads at the full input rate.
const tokenUsage = extractTokenUsage(usage);
const totalTokens = tokenUsage.total;
const actualCost = await this.calculateActualCost(tokenUsage);
recordProviderPerformanceFromMetrics(this.providerName, {
responseTime,
tokensGenerated: totalTokens,
cost: actualCost,
success: true,
});
logger.debug(`Performance recorded for ${this.providerName}`, {
responseTime: `${responseTime}ms`,
tokens: totalTokens,
estimatedCost: actualCost === undefined ? "unknown" : `$${actualCost.toFixed(6)}`,
});
}
catch (perfError) {
logger.warn("⚠️ Performance recording failed:", perfError);
}
}
/**
* Estimate cost only from known per-model rates. Unknown pricing remains
* absent from cost metrics instead of using another model's default rate.
*/
async calculateActualCost(usage) {
try {
// Try the per-model pricing table first (includes correct rates for
// Claude on Vertex, cache token rates, etc.)
if (hasPricing(this.providerName, this.modelName)) {
return calculateCost(this.providerName, this.modelName, usage);
}
return undefined;
}
catch (error) {
logger.debug(`Cost calculation failed for ${this.providerName}:`, error);
return undefined;
}
}
/**
* Handle tool execution storage if available
*/
async handleToolExecutionStorage(toolCalls, toolResults, options, currentTime) {
// Check if tools are not empty
const hasToolData = (toolCalls && toolCalls.length > 0) ||
(toolResults && toolResults.length > 0);
// Check if NeuroLink instance is available and has tool execution storage
const hasStorageAvailable = this.neurolink?.isToolExecutionStorageAvailable();
// Early return if storage is not available or no tool data
if (!hasStorageAvailable || !hasToolData || !this.neurolink) {
return;
}
const sessionId = options.context?.sessionId ||
("sessionId" in options ? options.sessionId : undefined) ||
`session-${nanoid()}`;
const userId = options.context?.userId ||
("userId" in options ? options.userId : undefined);
try {
await this.neurolink.storeToolExecutions(sessionId, userId, toolCalls, toolResults, currentTime);
}
catch (error) {
logger.warn("Failed to store tool executions:", error);
}
}
}