UNPKG

@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

117 lines (116 loc) • 4.77 kB
/** * NeuroLink Analytics System * * Provides lightweight analytics tracking for AI provider usage, * including tokens, costs, performance metrics, and custom context. */ import { logger } from "../utils/logger.js"; import { modelConfig } from "./modelConfiguration.js"; import { extractTokenUsage as extractTokenUsageUtil } from "../utils/tokenUtils.js"; import { calculateCost, hasPricing } from "../utils/pricing.js"; /** * Create analytics data structure from AI response */ export function createAnalytics(provider, model, result, responseTime, context) { const functionTag = "createAnalytics"; try { // Extract token usage from different result formats const tokens = extractTokenUsage(result); // Estimate cost based on provider and tokens const cost = estimateCost(provider, model, tokens); // Turn-lifecycle telemetry from native agentic loops (Vertex // Gemini/Claude): stopReason / rawFinishReason / stepsUsed ride the // provider result; toolCallCount and elapsedMs derive from it. const turnResult = result; const toolCallCount = Array.isArray(turnResult.toolExecutions) ? turnResult.toolExecutions.length : Array.isArray(turnResult.toolsUsed) ? turnResult.toolsUsed.length : undefined; const analytics = { provider, model, tokenUsage: tokens, cost, requestDuration: responseTime, context: context, timestamp: new Date().toISOString(), ...(typeof turnResult.stepsUsed === "number" && { stepsUsed: turnResult.stepsUsed, }), ...(toolCallCount !== undefined && { toolCallCount }), ...(typeof turnResult.stopReason === "string" && { stopReason: turnResult.stopReason, }), ...(typeof turnResult.responseTime === "number" && { elapsedMs: turnResult.responseTime, }), ...(typeof turnResult.rawFinishReason === "string" && { rawFinishReason: turnResult.rawFinishReason, }), }; logger.debug(`[${functionTag}] Analytics created`, { provider, model, tokens: tokens.total, responseTime, cost, }); return analytics; } catch (error) { logger.error(`[${functionTag}] Failed to create analytics`, { error }); // Return minimal analytics on error return { provider, model, tokenUsage: { input: 0, output: 0, total: 0 }, requestDuration: responseTime, context: context, timestamp: new Date().toISOString(), }; } } /** * Extract token usage from various AI result formats * Delegates to centralized tokenUtils for consistent extraction across providers */ function extractTokenUsage(result) { // Use centralized token extraction utility // The utility handles nested usage objects, multiple provider formats, // cache tokens, reasoning tokens, and cache savings calculation // Cast result to allow extractTokenUsageUtil to handle type normalization return extractTokenUsageUtil(result.usage); } /** * Estimate cost based on provider, model, and token usage. * Uses the per-model pricing table first (which includes cache token rates), * then falls back to the provider-level configuration system. */ function estimateCost(provider, model, tokens) { try { // Try the per-model pricing table first (includes cache token rates) if (hasPricing(provider, model)) { return calculateCost(provider, model, tokens); } // Fall back to the configuration system for providers/models not in the pricing table const costInfo = modelConfig.getCostInfo(provider.toLowerCase(), model); if (!costInfo) { return undefined; } // Calculate cost using the configuration system (per-1K-token rates). // costInfo has no cache tiers, so cache tokens are billed at the input // rate — the pre-split total a cache-blind provider would report, never $0. const inputCost = ((tokens.input + (tokens.cacheReadTokens ?? 0) + (tokens.cacheCreationTokens ?? 0)) / 1000) * costInfo.input; const outputCost = (tokens.output / 1000) * costInfo.output; return Math.round((inputCost + outputCost) * 1_000_000) / 1_000_000; // Round to 6 decimal places } catch (error) { logger.debug("Cost estimation failed", { provider, model, error }); return undefined; } }