cost-claude
Version:
Claude Code cost monitoring, analytics, and optimization toolkit
226 lines • 7.17 kB
JavaScript
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