UNPKG

llm-credit-sdk

Version:

SDK for estimating and reconciling LLM token costs using a credit system.

338 lines 13.9 kB
"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