UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

416 lines 14.2 kB
/** * Token Usage Tracker * Aggregates token usage and cost across spans */ /** * Built-in model pricing database (approximate, subject to change) */ const MODEL_PRICING = { // OpenAI "gpt-4o": { inputPricePerMillion: 2.5, outputPricePerMillion: 10.0, cachedInputPricePerMillion: 1.25, }, "gpt-4o-mini": { inputPricePerMillion: 0.15, outputPricePerMillion: 0.6, cachedInputPricePerMillion: 0.075, }, o1: { inputPricePerMillion: 15.0, outputPricePerMillion: 60.0, cachedInputPricePerMillion: 7.5, }, "o1-mini": { inputPricePerMillion: 3.0, outputPricePerMillion: 12.0, cachedInputPricePerMillion: 1.5, }, // Anthropic "claude-sonnet-4-20250514": { inputPricePerMillion: 3.0, outputPricePerMillion: 15.0, cachedInputPricePerMillion: 0.3, }, "claude-3-5-sonnet-20241022": { inputPricePerMillion: 3.0, outputPricePerMillion: 15.0, cachedInputPricePerMillion: 0.3, }, "claude-3-5-haiku-20241022": { inputPricePerMillion: 0.8, outputPricePerMillion: 4.0, cachedInputPricePerMillion: 0.08, }, // Google "gemini-2.5-flash": { inputPricePerMillion: 0.15, outputPricePerMillion: 0.6, }, "gemini-2.5-pro": { inputPricePerMillion: 1.25, outputPricePerMillion: 10.0, }, "gemini-2.0-flash-exp": { inputPricePerMillion: 0.15, outputPricePerMillion: 0.6, }, // Mistral "mistral-large-latest": { inputPricePerMillion: 2.0, outputPricePerMillion: 6.0, }, "mistral-small-latest": { inputPricePerMillion: 0.2, outputPricePerMillion: 0.6, }, }; /** * Token tracker for aggregating usage across spans */ export class TokenTracker { stats = { totalInputTokens: 0, totalOutputTokens: 0, totalTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, reasoningTokens: 0, totalCost: 0, byProvider: new Map(), byModel: new Map(), bySpanType: new Map(), }; customPricing = new Map(); /** * Set custom pricing for a single model * @param modelName - The model name (e.g., "gpt-4o", "claude-3-5-sonnet") * @param pricing - The pricing information */ setObservabilityModelPricing(modelName, pricing) { this.customPricing.set(modelName, pricing); } /** * Update pricing for an existing model (alias for setObservabilityModelPricing) * @param model - The model name * @param pricing - The new pricing information */ updatePricing(model, pricing) { this.customPricing.set(model, pricing); } /** * Load pricing configuration from a config object * Useful for loading pricing from environment or config files * @param config - Record of model names to pricing information */ loadPricingFromConfig(config) { for (const [model, pricing] of Object.entries(config)) { this.customPricing.set(model, pricing); } } /** * Get pricing for a specific model * @param model - The model name * @returns The pricing information or undefined if not found */ getModelPricing(model) { return this.customPricing.get(model) ?? MODEL_PRICING[model]; } /** * Get all available model pricing (custom + built-in) * @returns Record of all model pricing */ getAllPricing() { const allPricing = { ...MODEL_PRICING, }; // Custom pricing takes precedence const customPricingEntries = Array.from(this.customPricing.entries()); for (const [model, pricing] of customPricingEntries) { allPricing[model] = pricing; } return allPricing; } /** * Remove custom pricing for a model (falls back to built-in) * @param model - The model name to remove custom pricing for */ removeCustomPricing(model) { return this.customPricing.delete(model); } /** * Track token usage from a span */ trackSpan(span) { const attrs = span.attributes; const inputTokens = attrs["ai.tokens.input"] ?? 0; const outputTokens = attrs["ai.tokens.output"] ?? 0; const totalTokens = attrs["ai.tokens.total"] ?? inputTokens + outputTokens; const cacheRead = attrs["ai.tokens.cache_read"] ?? 0; const cacheCreation = attrs["ai.tokens.cache_creation"] ?? 0; const reasoning = attrs["ai.tokens.reasoning"] ?? 0; const cost = attrs["ai.cost.total"] ?? this.calculateCost(attrs, inputTokens, outputTokens, cacheRead); // Update totals this.stats.totalInputTokens += inputTokens; this.stats.totalOutputTokens += outputTokens; this.stats.totalTokens += totalTokens; this.stats.cacheReadTokens += cacheRead; this.stats.cacheCreationTokens += cacheCreation; this.stats.reasoningTokens += reasoning; this.stats.totalCost += cost; // Update by provider const provider = attrs["ai.provider"]; if (provider) { const existing = this.stats.byProvider.get(provider) ?? { provider, inputTokens: 0, outputTokens: 0, totalTokens: 0, cost: 0, requestCount: 0, }; existing.inputTokens += inputTokens; existing.outputTokens += outputTokens; existing.totalTokens += totalTokens; existing.cost += cost; existing.requestCount += 1; this.stats.byProvider.set(provider, existing); } // Update by model const model = attrs["ai.model"]; if (model) { const existing = this.stats.byModel.get(model) ?? { model, provider: provider ?? "unknown", inputTokens: 0, outputTokens: 0, totalTokens: 0, cost: 0, requestCount: 0, avgTokensPerRequest: 0, }; existing.inputTokens += inputTokens; existing.outputTokens += outputTokens; existing.totalTokens += totalTokens; existing.cost += cost; existing.requestCount += 1; existing.avgTokensPerRequest = existing.totalTokens / existing.requestCount; this.stats.byModel.set(model, existing); } // Update by span type const currentTypeTotal = this.stats.bySpanType.get(span.type) ?? 0; this.stats.bySpanType.set(span.type, currentTypeTotal + totalTokens); } /** * Calculate cost from token counts and provider/model */ calculateCost(attrs, inputTokens, outputTokens, cacheReadTokens = 0) { const model = attrs["ai.model"]; if (!model) { return 0; } // Check custom pricing first, then fall back to built-in const pricing = this.customPricing.get(model) ?? MODEL_PRICING[model]; if (!pricing) { return 0; } const regularInputTokens = inputTokens - cacheReadTokens; const regularCost = (regularInputTokens / 1_000_000) * pricing.inputPricePerMillion; const cachedCost = (cacheReadTokens / 1_000_000) * (pricing.cachedInputPricePerMillion ?? pricing.inputPricePerMillion); const outputCost = (outputTokens / 1_000_000) * pricing.outputPricePerMillion; return regularCost + cachedCost + outputCost; } /** * Track token usage from a simple usage object * This is a convenience method for tracking usage without a full span * @param usage - Token usage data */ trackUsage(usage) { const inputTokens = usage.promptTokens ?? 0; const outputTokens = usage.completionTokens ?? 0; const totalTokens = usage.totalTokens ?? inputTokens + outputTokens; // Update totals this.stats.totalInputTokens += inputTokens; this.stats.totalOutputTokens += outputTokens; this.stats.totalTokens += totalTokens; // Calculate cost if model is provided let cost = 0; if (usage.model) { const pricing = this.customPricing.get(usage.model) ?? MODEL_PRICING[usage.model]; if (pricing) { cost = (inputTokens / 1_000_000) * pricing.inputPricePerMillion + (outputTokens / 1_000_000) * pricing.outputPricePerMillion; } } this.stats.totalCost += cost; // Update by provider if provided if (usage.provider) { const existing = this.stats.byProvider.get(usage.provider) ?? { provider: usage.provider, inputTokens: 0, outputTokens: 0, totalTokens: 0, cost: 0, requestCount: 0, }; existing.inputTokens += inputTokens; existing.outputTokens += outputTokens; existing.totalTokens += totalTokens; existing.cost += cost; existing.requestCount += 1; this.stats.byProvider.set(usage.provider, existing); } // Update by model if provided if (usage.model) { const existing = this.stats.byModel.get(usage.model) ?? { model: usage.model, provider: usage.provider ?? "unknown", inputTokens: 0, outputTokens: 0, totalTokens: 0, cost: 0, requestCount: 0, avgTokensPerRequest: 0, }; existing.inputTokens += inputTokens; existing.outputTokens += outputTokens; existing.totalTokens += totalTokens; existing.cost += cost; existing.requestCount += 1; existing.avgTokensPerRequest = existing.totalTokens / existing.requestCount; this.stats.byModel.set(usage.model, existing); } } /** * Get current stats */ getStats() { return { ...this.stats }; } /** * Get stats for a specific time window of spans */ getStatsForWindow(spans) { const tracker = new TokenTracker(); // Copy custom pricing so windowed calculations use the same rates for (const [model, pricing] of this.customPricing) { tracker.setObservabilityModelPricing(model, pricing); } for (const span of spans) { tracker.trackSpan(span); } return tracker.getStats(); } /** * Reset all stats */ reset() { this.stats = { totalInputTokens: 0, totalOutputTokens: 0, totalTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, reasoningTokens: 0, totalCost: 0, byProvider: new Map(), byModel: new Map(), bySpanType: new Map(), }; } /** * Export stats as JSON */ toJSON() { return { totalInputTokens: this.stats.totalInputTokens, totalOutputTokens: this.stats.totalOutputTokens, totalTokens: this.stats.totalTokens, cacheReadTokens: this.stats.cacheReadTokens, cacheCreationTokens: this.stats.cacheCreationTokens, reasoningTokens: this.stats.reasoningTokens, totalCost: this.stats.totalCost, byProvider: Object.fromEntries(this.stats.byProvider), byModel: Object.fromEntries(this.stats.byModel), bySpanType: Object.fromEntries(this.stats.bySpanType), }; } /** * Format cost as currency string */ formatCost(cost, currency = "USD") { return new Intl.NumberFormat("en-US", { style: "currency", currency, minimumFractionDigits: 4, }).format(cost); } /** * Get a summary string of current stats */ getSummary() { const stats = this.stats; return [ `Token Usage Summary:`, ` Input tokens: ${stats.totalInputTokens.toLocaleString()}`, ` Output tokens: ${stats.totalOutputTokens.toLocaleString()}`, ` Total tokens: ${stats.totalTokens.toLocaleString()}`, ` Total cost: ${this.formatCost(stats.totalCost)}`, stats.cacheReadTokens > 0 ? ` Cache read tokens: ${stats.cacheReadTokens.toLocaleString()}` : "", stats.reasoningTokens > 0 ? ` Reasoning tokens: ${stats.reasoningTokens.toLocaleString()}` : "", ] .filter(Boolean) .join("\n"); } } /** * Enrich span with token usage attributes */ export function enrichSpanWithTokenUsage(span, usage) { return { ...span, attributes: { ...span.attributes, "ai.tokens.input": usage.promptTokens ?? 0, "ai.tokens.output": usage.completionTokens ?? 0, "ai.tokens.total": usage.totalTokens ?? (usage.promptTokens ?? 0) + (usage.completionTokens ?? 0), "ai.tokens.cache_creation": usage.cacheCreationTokens, "ai.tokens.cache_read": usage.cacheReadTokens, "ai.tokens.reasoning": usage.reasoningTokens, }, }; } /** * Global token tracker instance (singleton pattern from main) */ let globalTokenTracker = null; /** * Get the global token tracker instance */ export function getTokenTracker() { if (!globalTokenTracker) { globalTokenTracker = new TokenTracker(); } return globalTokenTracker; } /** * Reset the global token tracker (for testing) */ export function resetTokenTracker() { if (globalTokenTracker) { globalTokenTracker.reset(); } globalTokenTracker = null; } //# sourceMappingURL=tokenTracker.js.map