llm-credit-sdk
Version:
SDK for estimating and reconciling LLM token costs using a credit system.
338 lines • 13.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LLMCreditSDK = void 0;
const config_1 = require("./config");
const dashboardClient_1 = require("./dashboardClient");
/**
* Simple event emitter for internal SDK events
*/
class EventEmitter {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
off(event, callback) {
if (!this.events[event])
return;
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
emit(event, ...args) {
if (!this.events[event])
return;
this.events[event].forEach(callback => {
try {
callback(...args);
}
catch (error) {
console.error(`Event callback error for '${event}':`, error);
}
});
}
}
/**
* LLM Credit SDK - Main class for token usage tracking and credit estimation
*/
class LLMCreditSDK {
/**
* Initialize the SDK with optional custom configuration
* @param customConfig - Optional custom configuration to override defaults
*/
constructor(customConfig) {
this.dashboardClient = null;
this.dashboardSyncEnabled = false;
// Deep merge custom config with defaults
this.config = this.mergeConfigs(config_1.DEFAULT_CONFIG, customConfig || {});
this.eventEmitter = new EventEmitter();
// Listen for config updates
this.eventEmitter.on('configUpdated', (newConfig) => {
console.log('[LLMCreditSDK] Configuration updated from dashboard');
this.config = newConfig;
});
}
/**
* Estimate credits for an LLM call before making it
* @param input - Parameters for credit estimation (supports both standard and custom models/features)
* @returns Estimated number of credits
*/
estimateCredits(input) {
const { model, feature, promptTokens, completionTokens } = input;
// Get model configuration
const modelConfig = this.getModelConfig(model);
// Calculate base cost in dollars
const promptCost = (promptTokens / 1000) * modelConfig.prompt_cost_per_1k;
const completionCost = (completionTokens / 1000) * modelConfig.completion_cost_per_1k;
const baseCost = promptCost + completionCost;
// Get margin for this feature or use default
const margin = this.getMarginForFeature(modelConfig, feature);
// Calculate cost with margin in dollars
const costWithMargin = baseCost * margin;
// Convert dollars to credits using credit_per_dollar rate
const estimatedCredits = costWithMargin * this.config.credit_per_dollar;
return {
estimatedCredits: Number(estimatedCredits.toFixed(6))
};
}
/**
* Reconcile estimated vs actual usage after an LLM call
* @param input - Parameters for reconciliation (supports both standard and custom models/features)
* @returns Reconciliation results with actual usage and delta
*/
reconcile(input) {
const { model, feature, promptTokens, completionTokens, actualPromptTokens, actualCompletionTokens } = input;
// Get estimated credits
const estimated = this.estimateCredits({
model,
feature,
promptTokens,
completionTokens
});
// Calculate estimated cost (without margin)
const modelConfig = this.getModelConfig(model);
const estimatedPromptCost = (promptTokens / 1000) * modelConfig.prompt_cost_per_1k;
const estimatedCompletionCost = (completionTokens / 1000) * modelConfig.completion_cost_per_1k;
const estimatedCost = estimatedPromptCost + estimatedCompletionCost;
// Calculate actual usage
const actualPromptCost = (actualPromptTokens / 1000) * modelConfig.prompt_cost_per_1k;
const actualCompletionCost = (actualCompletionTokens / 1000) * modelConfig.completion_cost_per_1k;
const actualCost = actualPromptCost + actualCompletionCost;
const actualTokensUsed = actualPromptTokens + actualCompletionTokens;
// Get margin and calculate actual credits
const margin = this.getMarginForFeature(modelConfig, feature);
const actualCostWithMargin = actualCost * margin;
const actualCredits = actualCostWithMargin * this.config.credit_per_dollar;
// Calculate deltas
const estimatedVsActualCreditDelta = actualCredits - estimated.estimatedCredits;
const costDelta = actualCost - estimatedCost;
const marginDelta = (actualCredits / actualCost) - (estimated.estimatedCredits / estimatedCost);
return {
estimatedCredits: estimated.estimatedCredits,
actualTokensUsed,
actualCost: Number(actualCost.toFixed(6)),
estimatedVsActualCreditDelta: Number(estimatedVsActualCreditDelta.toFixed(6)),
costDelta: Number(costDelta.toFixed(6)),
marginDelta: Number(marginDelta.toFixed(6))
};
}
/**
* Wrap an LLM call with automatic credit estimation and reconciliation
* @param input - Parameters for the wrapped call (supports both standard and custom models/features)
* @returns Response from the LLM call plus reconciliation data
*/
async wrapCall(input) {
const { model, feature, promptTokens, completionTokens, callFunction, tokenExtractor } = input;
console.log(`[LLMCreditSDK] Wrapping call for ${model}:${feature} (estimated: ${promptTokens + completionTokens} tokens)`);
// Make the actual LLM call
const response = await callFunction();
// Extract actual token usage using the provided extractor
let actualPromptTokens = promptTokens;
let actualCompletionTokens = completionTokens;
if (tokenExtractor) {
const tokenUsage = tokenExtractor.extract(response);
actualPromptTokens = tokenUsage.promptTokens;
actualCompletionTokens = tokenUsage.completionTokens;
}
// If no extractor is provided, use the original estimates
// Reconcile actual vs estimated
const reconciliation = this.reconcile({
model,
feature,
promptTokens,
completionTokens,
actualPromptTokens,
actualCompletionTokens
});
// Post reconciliation data to dashboard (if enabled)
// This is non-blocking and won't affect the response
if (this.dashboardSyncEnabled) {
console.log(`[LLMCreditSDK] Sending reconciliation data for ${model}:${feature} to dashboard`);
}
this.postReconciliationToDashboard({
model,
feature,
promptTokens,
completionTokens,
actualPromptTokens,
actualCompletionTokens,
reconciliation
}).catch(error => {
// Error already logged in postReconciliationToDashboard
// This catch prevents unhandled promise rejection
});
return {
response,
reconciliation
};
}
/**
* Get configuration for a specific model
* @param model - Model name (e.g., 'openai:gpt-4')
* @returns Model configuration
* @throws Error if model is not found
*/
getModelConfig(model) {
const modelConfig = this.config.models[model];
if (!modelConfig) {
throw new Error(`Model '${model}' not found in configuration. Available models: ${Object.keys(this.config.models).join(', ')}`);
}
return modelConfig;
}
/**
* Get margin for a specific feature within a model
* @param modelConfig - Model configuration
* @param feature - Feature name
* @returns Margin multiplier
*/
getMarginForFeature(modelConfig, feature) {
const featureConfig = modelConfig.features?.[feature];
return featureConfig?.margin || this.config.default_margin;
}
/**
* Deep merge two configuration objects
* @param defaultConfig - Default configuration
* @param customConfig - Custom configuration to merge
* @returns Merged configuration
*/
mergeConfigs(defaultConfig, customConfig) {
const result = {
default_margin: customConfig.default_margin ?? defaultConfig.default_margin,
credit_per_dollar: customConfig.credit_per_dollar ?? defaultConfig.credit_per_dollar,
models: { ...defaultConfig.models }
};
// Merge custom models
if (customConfig.models) {
for (const [modelName, modelConfig] of Object.entries(customConfig.models)) {
if (result.models[modelName]) {
// Merge with existing model config
result.models[modelName] = {
...result.models[modelName],
...modelConfig,
features: {
...result.models[modelName].features,
...modelConfig.features
}
};
}
else {
// Add new model config
result.models[modelName] = modelConfig;
}
}
}
return result;
}
/**
* Get the current configuration (for debugging/inspection)
* @returns Current SDK configuration
*/
getConfig() {
return { ...this.config };
}
/**
* Get list of available models
* @returns Array of model names
*/
getAvailableModels() {
return Object.keys(this.config.models);
}
/**
* Get list of available features for a specific model
* @param model - Model name
* @returns Array of feature names
*/
getAvailableFeatures(model) {
const modelConfig = this.getModelConfig(model);
return Object.keys(modelConfig.features || {});
}
/**
* Get the current credit per dollar conversion rate
* @returns Credit per dollar rate
*/
getCreditPerDollar() {
return this.config.credit_per_dollar;
}
/**
* Enable dashboard synchronization
* @param config - Dashboard sync configuration
*/
enableDashboardSync(config) {
try {
this.dashboardClient = new dashboardClient_1.DashboardClient(config);
this.dashboardSyncEnabled = true;
// Subscribe to config updates from dashboard
this.dashboardClient.subscribeToConfigUpdates((newConfig) => {
this.eventEmitter.emit('configUpdated', newConfig);
});
console.log(`[LLMCreditSDK] Dashboard sync enabled with endpoint: ${config.endpoint}`);
}
catch (error) {
console.error('[LLMCreditSDK] Failed to enable dashboard sync:', error);
this.dashboardSyncEnabled = false;
}
}
/**
* Disable dashboard synchronization
*/
disableDashboardSync() {
if (this.dashboardClient) {
this.dashboardClient.unsubscribe();
this.dashboardClient = null;
}
this.dashboardSyncEnabled = false;
console.log('[LLMCreditSDK] Dashboard sync disabled');
}
/**
* Check if dashboard sync is enabled
* @returns True if dashboard sync is enabled
*/
isDashboardSyncEnabled() {
return this.dashboardSyncEnabled;
}
/**
* Get current dashboard configuration
* @returns Dashboard config if enabled, null otherwise
*/
getDashboardConfig() {
return this.dashboardClient?.['config'] || null;
}
/**
* Post reconciliation data to dashboard (if sync is enabled)
* This method is called internally but can also be used manually
*/
async postReconciliationToDashboard(reconciliationData) {
if (!this.dashboardSyncEnabled || !this.dashboardClient) {
return; // Silently skip if sync is disabled
}
try {
const log = {
model: reconciliationData.model,
feature: reconciliationData.feature,
promptTokens: reconciliationData.promptTokens,
completionTokens: reconciliationData.completionTokens,
actualPromptTokens: reconciliationData.actualPromptTokens,
actualCompletionTokens: reconciliationData.actualCompletionTokens,
estimatedCredits: reconciliationData.reconciliation.estimatedCredits,
actualTokensUsed: reconciliationData.reconciliation.actualTokensUsed,
actualCost: reconciliationData.reconciliation.actualCost,
estimatedVsActualCreditDelta: reconciliationData.reconciliation.estimatedVsActualCreditDelta,
costDelta: reconciliationData.reconciliation.costDelta,
marginDelta: reconciliationData.reconciliation.marginDelta,
creditPerDollar: this.getCreditPerDollar(),
timestamp: new Date().toISOString()
};
// Non-blocking call - errors are handled internally by DashboardClient
await this.dashboardClient.postReconciliation(log);
console.log(`[LLMCreditSDK] Reconciliation data successfully sent to dashboard for ${reconciliationData.model}:${reconciliationData.feature}`);
}
catch (error) {
// Log error but don't throw - maintain non-blocking behavior
console.error('[LLMCreditSDK] Failed to post reconciliation to dashboard:', error);
}
}
}
exports.LLMCreditSDK = LLMCreditSDK;
//# sourceMappingURL=sdk.js.map