/** * Logger interface for CostCalculator */ export interface CostLogger { debug: (message: string, meta?: Record) => void; error: (message: string, meta?: Record) => void; warn: (message: string, meta?: Record) => void; } // Default console logger const consoleLogger: CostLogger = { debug: (message: string, meta?: Record) => meta ? console.debug(message, meta) : console.debug(message), error: (message: string, meta?: Record) => meta ? console.error(message, meta) : console.error(message), warn: (message: string, meta?: Record) => meta ? console.warn(message, meta) : console.warn(message) }; let costLogger: CostLogger = consoleLogger; export const setCostLogger = (logger: CostLogger): void => { costLogger = logger; }; /** * Centralized cost calculation with accurate, up-to-date pricing * All pricing is per 1M tokens unless otherwise specified */ const SYNTHESIS_MODEL_NAMES = new Set(['multi-model-synthesis', 'step1-prompt-generation', 'step2-final-synthesis', 'step3-final-synthesis']); export class CostCalculator { // Current pricing as of 2024/2025 (updated regularly) private static readonly PRICING = { // Anthropic Claude Sonnet 4 pricing 'claude-sonnet-4': { input: 3.00, // $3 per 1M input tokens output: 15.00 // $15 per 1M output tokens }, 'multi-model-synthesis': { input: 5.00, // Grok pricing - $5 per 1M input tokens output: 15.00 // $15 per 1M output tokens }, 'claude-primary': { input: 3.00, output: 15.00 }, 'claude-secondary': { input: 3.00, output: 15.00 }, 'anthropic-claude': { input: 3.00, output: 15.00 }, // Gemini 2.5 Flash pricing 'gemini': { input: 0.075, // $0.075 per 1M input tokens output: 0.30 // $0.30 per 1M output tokens }, 'gemini-2.5-flash': { input: 0.075, output: 0.30 }, // Cerebras pricing (estimated based on latest info) 'cerebras': { input: 0.60, // $0.60 per 1M tokens (combined) output: 0.60 }, // Grok pricing (based on xAI published rates) 'grok-3': { input: 5.00, output: 15.00 }, // Perplexity Sonar pricing (https://docs.perplexity.ai/getting-started/pricing) 'sonar': { input: 1.00, output: 1.00 }, // OpenAI GPT-4o pricing (https://platform.openai.com/docs/pricing) 'openai-gpt-4o': { input: 5.00, // $5 per 1M input tokens output: 15.00 // $15 per 1M output tokens }, 'sonar-pro': { input: 3.00, output: 15.00 }, 'sonar-reasoning': { input: 1.00, output: 5.00 } }; /** * Calculate accurate cost for a model response */ static calculateCost( model: string, inputTokens: number = 0, outputTokens: number = 0, totalTokens?: number ): number { try { // Normalize model name const normalizedModel = this.normalizeModelName(model); const pricing = this.PRICING[normalizedModel as keyof typeof this.PRICING]; if (!pricing) { costLogger.warn(`Unknown model pricing for: ${model}, using fallback`, { model, normalizedModel, availableModels: Object.keys(this.PRICING) }); // Fallback to average pricing return this.calculateFallbackCost(totalTokens || (inputTokens + outputTokens)); } // If we only have total tokens, estimate 75% input / 25% output split if (totalTokens && !inputTokens && !outputTokens) { inputTokens = Math.floor(totalTokens * 0.75); outputTokens = Math.floor(totalTokens * 0.25); } // Calculate cost with proper precision const inputCost = (inputTokens / 1_000_000) * pricing.input; const outputCost = (outputTokens / 1_000_000) * pricing.output; const totalCost = inputCost + outputCost; // Round to 8 decimal places to prevent floating point errors const roundedCost = Math.round(totalCost * 100_000_000) / 100_000_000; costLogger.debug('Cost calculation', { model: normalizedModel, inputTokens, outputTokens, inputCost: inputCost.toFixed(8), outputCost: outputCost.toFixed(8), totalCost: roundedCost.toFixed(8) }); return roundedCost; } catch (error) { costLogger.error('Cost calculation failed', { error: error instanceof Error ? error.message : 'Unknown error', model, inputTokens, outputTokens, totalTokens }); return this.calculateFallbackCost(totalTokens || (inputTokens + outputTokens)); } } /** * Normalize model names to match pricing keys, with optional primary model context for synthesis operations */ private static normalizeModelName(model: string, primaryModel?: string): string { const normalized = model.toLowerCase().trim(); const normalizedWhitespaceCollapsed = normalized.replace(/\s+/g, ' '); const normalizedDashed = normalizedWhitespaceCollapsed.replace(/_/g, '-'); const normalizedHyphenTight = normalizedDashed.replace(/\s*-\s*/g, '-'); const normalizedSpaceless = normalizedDashed.replace(/\s+/g, ''); // For synthesis operations, use the actual primary model's pricing instead of fixed mapping if (primaryModel && SYNTHESIS_MODEL_NAMES.has(normalized)) { return this.normalizeModelName(primaryModel); // Recursively normalize the primary model } // Map various model names to canonical pricing keys const modelMappings: Record = { 'claude': 'claude-sonnet-4', 'claude-primary': 'claude-sonnet-4', 'claude-secondary': 'claude-sonnet-4', 'grok': 'grok-3', 'grok-3': 'grok-3', 'grok-primary': 'grok-3', 'anthropic-claude': 'claude-sonnet-4', 'gemini-2.5-flash': 'gemini', 'perplexity': 'sonar', 'perplexity-sonar': 'sonar', 'sonar': 'sonar', 'sonar-pro': 'sonar-pro', 'perplexity-sonar-pro': 'sonar-pro', 'sonar-reasoning': 'sonar-reasoning', 'perplexity-sonar-reasoning': 'sonar-reasoning', 'llama-3.1-sonar-large-128k-online': 'sonar-pro', // Legacy model name 'cerebras': 'cerebras', 'cerebras-primary': 'cerebras', 'cerebras-secondary': 'cerebras', 'cerebras - qwen 3 thinking': 'cerebras', 'cerebras-qwen-3-thinking': 'cerebras', 'cerebrasqwen3thinking': 'cerebras', 'qwen/qwen3-235b-a22b-thinking-2507': 'cerebras', 'qwen-3-235b-a22b-thinking-2507': 'cerebras', 'openai': 'openai-gpt-4o', 'openai-gpt-4o': 'openai-gpt-4o', 'gpt-4o': 'openai-gpt-4o', 'gpt-4o-mini': 'openai-gpt-4o', 'gpt-4o-mini-high': 'openai-gpt-4o', 'gpt-5': 'openai-gpt-4o', 'gpt5': 'openai-gpt-4o', 'gpt-5.1': 'openai-gpt-4o' }; const candidates = [ normalized, normalizedWhitespaceCollapsed, normalizedDashed, normalizedHyphenTight, normalizedSpaceless ]; for (const candidate of candidates) { const mapped = modelMappings[candidate]; if (mapped) { return mapped; } } return normalizedHyphenTight; } /** * Fallback cost calculation for unknown models */ private static calculateFallbackCost(totalTokens: number): number { // Use a conservative average pricing of $2 per 1M tokens const fallbackRate = 2.00 / 1_000_000; const cost = totalTokens * fallbackRate; costLogger.warn(`Using fallback cost calculation: ${totalTokens} tokens = $${cost.toFixed(8)}`); return Math.round(cost * 100_000_000) / 100_000_000; } /** * Validate and sanitize cost values */ static validateCost(cost: any, context: string = 'unknown'): number { if (typeof cost === 'number' && !isNaN(cost) && isFinite(cost) && cost >= 0) { // Round to 8 decimal places for consistency return Math.round(cost * 100_000_000) / 100_000_000; } costLogger.warn(`Invalid cost value detected in ${context}`, { cost, type: typeof cost, context }); return 0; } /** * Calculate total cost with validation across multiple responses */ static calculateTotalCost( responses: Array<{ cost?: number; model?: string; tokens?: number }>, primaryModel?: string ): number { let totalCost = 0; for (const response of responses) { let cost = response.cost; const tokens = response.tokens || 0; const modelName = response.model; const isSynthesisModel = Boolean(primaryModel) && typeof modelName === 'string' && SYNTHESIS_MODEL_NAMES.has(modelName); // For synthesis operations, recalculate cost using actual primary model pricing with total token count if (isSynthesisModel && primaryModel) { cost = this.calculateCost(primaryModel, 0, 0, tokens); } else if ((cost === undefined || cost === null) && typeof modelName === 'string' && tokens > 0) { // When a model omits cost metadata, derive it from the reported tokens cost = this.calculateCost(modelName, 0, 0, tokens); } const validatedCost = this.validateCost(cost, `${response.model || 'unknown'} response`); totalCost += validatedCost; } return Math.round(totalCost * 100_000_000) / 100_000_000; } /** * Get pricing information for a model */ static getPricing(model: string): { input: number; output: number } | null { const normalizedModel = this.normalizeModelName(model); return this.PRICING[normalizedModel as keyof typeof this.PRICING] || null; } /** * Estimate cost before making API call */ static estimateCost(model: string, estimatedInputTokens: number, estimatedOutputTokens: number = 0): number { return this.calculateCost(model, estimatedInputTokens, estimatedOutputTokens); } }