UNPKG

cost-claude

Version:

Claude Code cost monitoring, analytics, and optimization toolkit

226 lines 7.17 kB
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; import fetch from 'node-fetch'; const DEFAULT_CLAUDE_PRICING = { 'claude-opus-4-20250514': { modelId: 'claude-opus-4-20250514', modelName: 'Claude Opus 4', input: 15.00, output: 60.00, cacheCreation: 15.00, cacheRead: 1.50, perTokens: 1_000_000, currency: 'USD', }, 'claude-3-opus-20240229': { modelId: 'claude-3-opus-20240229', modelName: 'Claude 3 Opus', input: 15.00, output: 75.00, cacheCreation: 18.75, cacheRead: 1.875, perTokens: 1_000_000, currency: 'USD', }, 'claude-3-5-sonnet-20241022': { modelId: 'claude-3-5-sonnet-20241022', modelName: 'Claude 3.5 Sonnet', input: 3.00, output: 15.00, cacheCreation: 3.75, cacheRead: 0.30, perTokens: 1_000_000, currency: 'USD', }, 'claude-3-5-haiku-20241022': { modelId: 'claude-3-5-haiku-20241022', modelName: 'Claude 3.5 Haiku', input: 1.00, output: 5.00, cacheCreation: 1.25, cacheRead: 0.10, perTokens: 1_000_000, currency: 'USD', }, }; export class PricingService { static instance; cachePath; cacheDir; currentModel; cache = null; CACHE_DURATION_MS = 30 * 24 * 60 * 60 * 1000; PRICING_URLS = [ 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json', ]; constructor(model = 'claude-opus-4-20250514') { this.currentModel = model; this.cacheDir = join(homedir(), '.cost-claude', 'cache'); this.cachePath = join(this.cacheDir, 'pricing-cache.json'); this.ensureCacheDirectory(); this.loadCache(); } static getInstance(model) { if (!PricingService.instance) { PricingService.instance = new PricingService(model); } else if (model && model !== PricingService.instance.currentModel) { PricingService.instance.setModel(model); } return PricingService.instance; } ensureCacheDirectory() { if (!existsSync(this.cacheDir)) { mkdirSync(this.cacheDir, { recursive: true }); } } loadCache() { if (existsSync(this.cachePath)) { try { const cacheData = JSON.parse(readFileSync(this.cachePath, 'utf-8')); const expiresAt = new Date(cacheData.expiresAt); if (expiresAt > new Date()) { this.cache = cacheData; } else { console.info('Pricing cache expired, will refresh'); } } catch (error) { console.warn('Failed to load pricing cache:', error); } } } saveCache() { if (this.cache) { try { writeFileSync(this.cachePath, JSON.stringify(this.cache, null, 2)); } catch (error) { console.error('Failed to save pricing cache:', error); } } } async fetchRemotePricing() { for (const url of this.PRICING_URLS) { try { const response = await fetch(url); if (!response.ok) continue; return null; } catch (error) { console.warn(`Failed to fetch pricing from ${url}:`, error); } } return null; } getDefaultPricing() { const models = {}; const now = new Date().toISOString(); for (const [modelId, pricing] of Object.entries(DEFAULT_CLAUDE_PRICING)) { models[modelId] = { ...pricing, lastUpdated: now, source: 'default', }; } return models; } async refreshPricing() { const remotePricing = await this.fetchRemotePricing(); const now = new Date(); const expiresAt = new Date(now.getTime() + this.CACHE_DURATION_MS); if (remotePricing && Object.keys(remotePricing).length > 0) { this.cache = { models: remotePricing, lastFetch: now.toISOString(), expiresAt: expiresAt.toISOString(), }; } else { this.cache = { models: this.getDefaultPricing(), lastFetch: now.toISOString(), expiresAt: expiresAt.toISOString(), }; } this.saveCache(); } async ensurePricing() { if (!this.cache || new Date(this.cache.expiresAt) <= new Date()) { await this.refreshPricing(); } } async getPricing(modelId) { await this.ensurePricing(); const targetModel = modelId || this.currentModel; if (!this.cache) { const defaults = this.getDefaultPricing(); return defaults[targetModel] || null; } return this.cache.models[targetModel] || null; } async getRateConfig(modelId) { const pricing = await this.getPricing(modelId); if (!pricing) { return null; } return { input: pricing.input, output: pricing.output, cacheCreation: pricing.cacheCreation || pricing.input, cacheRead: pricing.cacheRead || pricing.input * 0.1, perTokens: pricing.perTokens, currency: pricing.currency, lastUpdated: pricing.lastUpdated, }; } async getAllModels() { await this.ensurePricing(); if (!this.cache) { return Object.values(this.getDefaultPricing()); } return Object.values(this.cache.models); } setModel(modelId) { this.currentModel = modelId; } getModel() { return this.currentModel; } async addCustomPricing(pricing) { await this.ensurePricing(); if (!this.cache) { this.cache = { models: {}, lastFetch: new Date().toISOString(), expiresAt: new Date(Date.now() + this.CACHE_DURATION_MS).toISOString(), }; } this.cache.models[pricing.modelId] = { ...pricing, source: 'local', lastUpdated: new Date().toISOString(), }; this.saveCache(); } clearCache() { this.cache = null; if (existsSync(this.cachePath)) { try { const invalidCache = { models: {}, lastFetch: new Date().toISOString(), expiresAt: new Date(0).toISOString(), }; writeFileSync(this.cachePath, JSON.stringify(invalidCache, null, 2)); } catch (error) { console.error('Failed to clear cache:', error); } } } } //# sourceMappingURL=pricing-service.js.map