UNPKG

@tehreet/conduit

Version:

LLM API gateway with intelligent routing, robust process management, and health monitoring

237 lines 8.51 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.SynapseFeatures = void 0; const events_1 = require("events"); const fs = __importStar(require("fs/promises")); const path = __importStar(require("path")); const log_1 = require("../utils/log"); class SynapseFeatures extends events_1.EventEmitter { constructor(config = {}) { super(); // Model pricing (per 1M tokens) this.modelPricing = { 'claude-3-5-sonnet-20241022': { input: 3.0, output: 15.0 }, 'claude-3-5-haiku-20241022': { input: 0.25, output: 1.25 }, 'claude-3-opus-20240229': { input: 15.0, output: 75.0 }, }; const dataDir = config.dataDir || path.join(process.env.HOME || '', '.conduit', 'synapse-data'); this.usageLogPath = path.join(dataDir, 'usage.ndjson'); this.performanceLogPath = path.join(dataDir, 'performance.ndjson'); this.telemetryEndpoint = config.telemetryEndpoint; this.ensureDataDir(dataDir); } async ensureDataDir(dir) { try { await fs.mkdir(dir, { recursive: true }); } catch (error) { (0, log_1.log)('Error creating data directory:', error); } } /** * Track model usage for a request */ async trackUsage(params) { const outputTokens = params.outputTokens || 0; const record = { ...params, outputTokens, timestamp: new Date().toISOString(), cost: this.calculateCost(params.model, params.inputTokens, outputTokens), }; // Append to local log await this.appendToLog(this.usageLogPath, record); // Send telemetry if configured if (this.telemetryEndpoint) { this.sendTelemetry('usage', record); } // Emit usage event this.emit('usage', record); } /** * Get project costs for a time period */ async getProjectCosts(projectId, since) { const usage = await this.queryUsage({ projectId, since }); const report = { totalCost: 0, costByModel: {}, costByAgent: {}, tokenUsage: { input: 0, output: 0, total: 0 }, }; for (const record of usage) { report.totalCost += record.cost || 0; // Aggregate by model if (!report.costByModel[record.model]) { report.costByModel[record.model] = 0; } report.costByModel[record.model] += record.cost || 0; // Aggregate by agent if (!report.costByAgent[record.agentId]) { report.costByAgent[record.agentId] = 0; } report.costByAgent[record.agentId] += record.cost || 0; // Token usage report.tokenUsage.input += record.inputTokens; report.tokenUsage.output += record.outputTokens; } report.tokenUsage.total = report.tokenUsage.input + report.tokenUsage.output; return report; } /** * Record performance metrics */ async recordPerformance(params) { await this.appendToLog(this.performanceLogPath, params); if (this.telemetryEndpoint) { this.sendTelemetry('performance', params); } this.emit('performance', params); } /** * Query usage records */ async queryUsage(filters) { try { const content = await fs.readFile(this.usageLogPath, 'utf-8'); const lines = content.split('\n').filter(line => line.trim()); const records = []; for (const line of lines) { try { const record = JSON.parse(line); // Apply filters if (filters.projectId && record.projectId !== filters.projectId) continue; if (filters.agentId && record.agentId !== filters.agentId) continue; if (filters.since && new Date(record.timestamp) < filters.since) continue; records.push(record); } catch (error) { // Skip invalid records } } return records; } catch (error) { if (error.code === 'ENOENT') { return []; // File doesn't exist yet } throw error; } } /** * Calculate cost for token usage */ calculateCost(model, inputTokens, outputTokens) { const pricing = this.modelPricing[model]; if (!pricing) return 0; const inputCost = (inputTokens / 1000000) * pricing.input; const outputCost = (outputTokens / 1000000) * pricing.output; return Number((inputCost + outputCost).toFixed(6)); } /** * Append record to NDJSON log file */ async appendToLog(filePath, record) { try { const line = JSON.stringify(record) + '\n'; await fs.appendFile(filePath, line); } catch (error) { (0, log_1.log)('Error appending to log:', error); } } /** * Send telemetry to Synapse */ async sendTelemetry(event, data) { if (!this.telemetryEndpoint) return; try { const response = await fetch(this.telemetryEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ event, data, timestamp: new Date().toISOString(), }), }); if (!response.ok) { (0, log_1.log)(`Telemetry failed: ${response.status} ${response.statusText}`); } } catch (error) { (0, log_1.log)('Telemetry error:', error); // Don't fail on telemetry errors } } /** * Get usage statistics */ async getStats(projectId, days = 30) { const since = new Date(); since.setDate(since.getDate() - days); const usage = await this.queryUsage({ projectId, since }); const stats = { totalRequests: usage.length, totalTokens: 0, totalCost: 0, averageTokensPerRequest: 0, modelDistribution: {}, }; for (const record of usage) { stats.totalTokens += record.inputTokens + record.outputTokens; stats.totalCost += record.cost || 0; if (!stats.modelDistribution[record.model]) { stats.modelDistribution[record.model] = 0; } stats.modelDistribution[record.model]++; } stats.averageTokensPerRequest = stats.totalRequests > 0 ? Math.round(stats.totalTokens / stats.totalRequests) : 0; return stats; } } exports.SynapseFeatures = SynapseFeatures; //# sourceMappingURL=synapse-features.js.map