@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
JavaScript
/**
* 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;
}
}