@just-every/ensemble
Version:
LLM provider abstraction layer with unified streaming interface
244 lines • 10.6 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.CostTracker = exports.costTracker = void 0;
const model_data_js_1 = require("../data/model_data.cjs");
const event_controller_js_1 = require("./event_controller.cjs");
class CostTracker {
entries = [];
started = new Date();
onAddUsageCallbacks = [];
calculateCost(usage) {
if (typeof usage.cost === 'number') {
return usage;
}
if (usage.isFreeTierUsage) {
usage.cost = 0;
return usage;
}
const model = (0, model_data_js_1.findModel)(usage.model);
if (!model) {
console.error(`Model not found when recording usage: ${usage.model}`);
throw new Error(`Model not found when recording usage: ${usage.model}`);
}
if (usage.cost !== undefined && usage.cost !== null && usage.cost > 0) {
return usage;
}
usage.cost = 0;
const original_input_tokens = usage.input_tokens || 0;
const output_tokens = usage.output_tokens || 0;
const cached_tokens = usage.cached_tokens || 0;
const image_count = usage.image_count || 0;
const calculationTime = usage.timestamp || new Date();
const usesTimeBasedPricing = (typeof model.cost?.input_per_million === 'object' &&
model.cost.input_per_million !== null &&
'peak_price_per_million' in model.cost.input_per_million) ||
(typeof model.cost?.output_per_million === 'object' &&
model.cost.output_per_million !== null &&
'peak_price_per_million' in model.cost.output_per_million) ||
(typeof model.cost?.cached_input_per_million === 'object' &&
model.cost.cached_input_per_million !== null &&
'peak_price_per_million' in model.cost.cached_input_per_million);
if (!usage.timestamp && usesTimeBasedPricing) {
}
const getPrice = (tokensForTierCheck, costStructure, modality) => {
if (typeof costStructure === 'number') {
return costStructure;
}
if (typeof costStructure === 'object' && costStructure !== null) {
if ('text' in costStructure ||
'audio' in costStructure ||
'video' in costStructure ||
'image' in costStructure) {
const modalityPrice = costStructure;
const selectedModality = modality || 'text';
const modalityCost = modalityPrice[selectedModality];
if (modalityCost !== undefined) {
return getPrice(tokensForTierCheck, modalityCost);
}
return getPrice(tokensForTierCheck, modalityPrice.text || 0);
}
if ('peak_price_per_million' in costStructure) {
const timeBasedCost = costStructure;
const utcHour = calculationTime.getUTCHours();
const utcMinute = calculationTime.getUTCMinutes();
const currentTimeInMinutes = utcHour * 60 + utcMinute;
const peakStartInMinutes = timeBasedCost.peak_utc_start_hour * 60 + timeBasedCost.peak_utc_start_minute;
const peakEndInMinutes = timeBasedCost.peak_utc_end_hour * 60 + timeBasedCost.peak_utc_end_minute;
let isPeakTime;
if (peakStartInMinutes <= peakEndInMinutes) {
isPeakTime =
currentTimeInMinutes >= peakStartInMinutes && currentTimeInMinutes < peakEndInMinutes;
}
else {
isPeakTime =
currentTimeInMinutes >= peakStartInMinutes || currentTimeInMinutes < peakEndInMinutes;
}
return isPeakTime ? timeBasedCost.peak_price_per_million : timeBasedCost.off_peak_price_per_million;
}
else if ('threshold_tokens' in costStructure) {
const tieredCost = costStructure;
if (tokensForTierCheck <= tieredCost.threshold_tokens) {
return tieredCost.price_below_threshold_per_million;
}
else {
return tieredCost.price_above_threshold_per_million;
}
}
}
return 0;
};
let nonCachedInputTokens = 0;
let actualCachedTokens = 0;
if (cached_tokens > 0 && model.cost?.cached_input_per_million !== undefined) {
actualCachedTokens = cached_tokens;
nonCachedInputTokens = Math.max(0, original_input_tokens - cached_tokens);
}
else {
nonCachedInputTokens = original_input_tokens;
actualCachedTokens = 0;
}
if (nonCachedInputTokens > 0 && model.cost?.input_per_million !== undefined) {
const inputPricePerMillion = getPrice(original_input_tokens, model.cost.input_per_million, usage.input_modality);
usage.cost += (nonCachedInputTokens / 1000000) * inputPricePerMillion;
}
if (actualCachedTokens > 0 && model.cost?.cached_input_per_million !== undefined) {
const cachedPricePerMillion = getPrice(actualCachedTokens, model.cost.cached_input_per_million);
usage.cost += (actualCachedTokens / 1000000) * cachedPricePerMillion;
}
if (output_tokens > 0 && model.cost?.output_per_million !== undefined) {
const outputPricePerMillion = getPrice(output_tokens, model.cost.output_per_million, usage.output_modality);
usage.cost += (output_tokens / 1000000) * outputPricePerMillion;
}
if (image_count > 0 && model.cost?.per_image) {
usage.cost += image_count * model.cost.per_image;
}
usage.cost = Math.max(0, usage.cost);
return usage;
}
onAddUsage(callback) {
this.onAddUsageCallbacks.push(callback);
}
addUsage(usage) {
try {
usage = this.calculateCost({ ...usage });
usage.timestamp = new Date();
this.entries.push(usage);
if ((0, event_controller_js_1.hasEventHandler)()) {
const costUpdateEvent = {
type: 'cost_update',
usage: {
...usage,
total_tokens: (usage.input_tokens || 0) + (usage.output_tokens || 0),
},
timestamp: new Date().toISOString(),
};
(0, event_controller_js_1.emitEvent)(costUpdateEvent).catch(error => {
console.error('Error emitting cost_update event:', error);
});
}
for (const callback of this.onAddUsageCallbacks) {
try {
callback(usage);
}
catch (error) {
console.error('Error in cost tracker callback:', error);
}
}
return usage;
}
catch (err) {
console.error('Error recording usage:', err);
return usage;
}
}
getTotalCost() {
return this.entries.reduce((sum, entry) => sum + (entry.cost || 0), 0);
}
getCostsByModel() {
const models = {};
for (const entry of this.entries) {
if (!models[entry.model]) {
models[entry.model] = {
cost: 0,
calls: 0,
};
}
models[entry.model].cost += entry.cost || 0;
models[entry.model].calls += 1;
}
return models;
}
printSummary() {
if (!this.entries.length) {
return;
}
const totalCost = this.getTotalCost();
const costsByModel = this.getCostsByModel();
const runtime = Math.round((new Date().getTime() - this.started.getTime()) / 1000);
console.log('\n\nCOST SUMMARY');
console.log(`Runtime: ${runtime} seconds`);
console.log(`Total API Cost: $${totalCost.toFixed(6)}`);
console.log('\nModels:');
for (const [model, modelData] of Object.entries(costsByModel)) {
console.log(`\t${model}:\t$${modelData.cost.toFixed(6)} (${modelData.calls} calls)`);
}
this.reset();
}
getCostInTimeWindow(seconds) {
const cutoffTime = new Date(Date.now() - seconds * 1000);
return this.entries
.filter(entry => entry.timestamp && entry.timestamp >= cutoffTime)
.reduce((sum, entry) => sum + (entry.cost || 0), 0);
}
getCostRate(windowSeconds = 60) {
const costInWindow = this.getCostInTimeWindow(windowSeconds);
return (costInWindow / windowSeconds) * 60;
}
getUsageInTimeWindow(seconds) {
const cutoffTime = new Date(Date.now() - seconds * 1000);
return this.entries.filter(entry => entry.timestamp && entry.timestamp >= cutoffTime);
}
getCostsByModelInTimeWindow(seconds) {
const models = {};
const entriesInWindow = this.getUsageInTimeWindow(seconds);
for (const entry of entriesInWindow) {
if (!models[entry.model]) {
models[entry.model] = {
cost: 0,
calls: 0,
};
}
models[entry.model].cost += entry.cost || 0;
models[entry.model].calls += 1;
}
return models;
}
reset() {
this.entries = [];
this.started = new Date();
}
static estimateTokens(text) {
if (!text)
return 0;
return Math.ceil(text.length / 4);
}
addEstimatedUsage(model, inputText, outputText, metadata) {
const usage = {
model,
input_tokens: CostTracker.estimateTokens(inputText),
output_tokens: CostTracker.estimateTokens(outputText),
metadata: {
...metadata,
estimated: true,
},
};
return this.addUsage(usage);
}
}
exports.CostTracker = CostTracker;
const globalObj = globalThis;
if (!globalObj.__ENSEMBLE_COST_TRACKER__) {
globalObj.__ENSEMBLE_COST_TRACKER__ = new CostTracker();
}
exports.costTracker = globalObj.__ENSEMBLE_COST_TRACKER__;
//# sourceMappingURL=cost_tracker.js.map
;