@tehreet/conduit
Version:
LLM API gateway with intelligent routing, robust process management, and health monitoring
237 lines • 8.51 kB
JavaScript
;
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