UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

550 lines (478 loc) 16.2 kB
#!/usr/bin/env node /** * DeepSeek Usage Tracker Proxy * * A lightweight HTTP proxy that tracks API usage per alias (e.g., "claude-free"). * Intercepts requests to DeepSeek API and logs token usage to local JSON file. * * @module usage-tracker * @location .aios-core/infrastructure/scripts/llm-routing/usage-tracker/ */ const http = require('http'); const https = require('https'); const fs = require('fs'); const path = require('path'); const os = require('os'); const url = require('url'); // Configuration const DEFAULT_PORT = 8787; const DEEPSEEK_HOST = 'api.deepseek.com'; const DATA_DIR = path.join(os.homedir(), '.aios', 'usage-tracking'); const USAGE_FILE = path.join(DATA_DIR, 'deepseek-usage.json'); // Pricing per million tokens (as of Dec 2025) const PRICING = { 'deepseek-chat': { input: 0.07, // $0.07 per 1M input tokens output: 0.14, // $0.14 per 1M output tokens cached: 0.014 // $0.014 per 1M cached tokens }, 'deepseek-reasoner': { input: 0.55, // $0.55 per 1M input tokens output: 2.19, // $2.19 per 1M output tokens cached: 0.14 // $0.14 per 1M cached tokens } }; /** * Ensure data directory exists */ function ensureDataDir() { if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, { recursive: true }); } } /** * Load existing usage data * @returns {Object} Usage data */ function loadUsageData() { ensureDataDir(); if (fs.existsSync(USAGE_FILE)) { try { return JSON.parse(fs.readFileSync(USAGE_FILE, 'utf8')); } catch (error) { console.error('Error loading usage data:', error.message); } } return { version: '1.0.0', created: new Date().toISOString(), aliases: {}, totalRequests: 0, totalTokens: { input: 0, output: 0, cached: 0 }, totalCost: 0 }; } /** * Save usage data * @param {Object} data - Usage data to save */ function saveUsageData(data) { ensureDataDir(); data.lastUpdated = new Date().toISOString(); fs.writeFileSync(USAGE_FILE, JSON.stringify(data, null, 2)); } /** * Calculate cost for token usage * @param {string} model - Model name * @param {Object} usage - Token usage object * @returns {number} Cost in USD */ function calculateCost(model, usage) { const pricing = PRICING[model] || PRICING['deepseek-chat']; const inputCost = (usage.prompt_tokens || 0) / 1_000_000 * pricing.input; const outputCost = (usage.completion_tokens || 0) / 1_000_000 * pricing.output; const cachedCost = (usage.prompt_cache_hit_tokens || 0) / 1_000_000 * pricing.cached; return inputCost + outputCost + cachedCost; } /** * Record usage for an alias * @param {string} alias - API key alias (e.g., "claude-free") * @param {Object} request - Request data * @param {Object} response - Response data with usage */ function recordUsage(alias, request, response) { const data = loadUsageData(); // Initialize alias if not exists if (!data.aliases[alias]) { data.aliases[alias] = { created: new Date().toISOString(), requests: 0, tokens: { input: 0, output: 0, cached: 0 }, cost: 0, byModel: {}, byDay: {} }; } const aliasData = data.aliases[alias]; const usage = response.usage || {}; const model = request.model || 'deepseek-chat'; const today = new Date().toISOString().split('T')[0]; const cost = calculateCost(model, usage); // Update alias totals aliasData.requests++; aliasData.tokens.input += usage.prompt_tokens || 0; aliasData.tokens.output += usage.completion_tokens || 0; aliasData.tokens.cached += usage.prompt_cache_hit_tokens || 0; aliasData.cost += cost; // Update by model if (!aliasData.byModel[model]) { aliasData.byModel[model] = { requests: 0, tokens: { input: 0, output: 0 }, cost: 0 }; } aliasData.byModel[model].requests++; aliasData.byModel[model].tokens.input += usage.prompt_tokens || 0; aliasData.byModel[model].tokens.output += usage.completion_tokens || 0; aliasData.byModel[model].cost += cost; // Update by day if (!aliasData.byDay[today]) { aliasData.byDay[today] = { requests: 0, tokens: { input: 0, output: 0 }, cost: 0 }; } aliasData.byDay[today].requests++; aliasData.byDay[today].tokens.input += usage.prompt_tokens || 0; aliasData.byDay[today].tokens.output += usage.completion_tokens || 0; aliasData.byDay[today].cost += cost; // Update global totals data.totalRequests++; data.totalTokens.input += usage.prompt_tokens || 0; data.totalTokens.output += usage.completion_tokens || 0; data.totalTokens.cached += usage.prompt_cache_hit_tokens || 0; data.totalCost += cost; saveUsageData(data); // Log to console console.log(`[${alias}] ${model}: ${usage.prompt_tokens || 0} in / ${usage.completion_tokens || 0} out = $${cost.toFixed(6)}`); } /** * Proxy request to DeepSeek API * @param {Object} req - Incoming request * @param {Object} res - Outgoing response * @param {string} alias - API key alias */ function proxyRequest(req, res, alias) { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { let requestData = {}; try { if (body) { requestData = JSON.parse(body); } } catch (e) { // Not JSON, continue anyway } // Build proxy request options const targetPath = req.url.replace(/^\/anthropic/, ''); const options = { hostname: DEEPSEEK_HOST, port: 443, path: '/anthropic' + targetPath, method: req.method, headers: { ...req.headers, host: DEEPSEEK_HOST } }; // Remove problematic headers delete options.headers['content-length']; if (body) { options.headers['content-length'] = Buffer.byteLength(body); } const proxyReq = https.request(options, (proxyRes) => { let responseBody = ''; proxyRes.on('data', chunk => { responseBody += chunk.toString(); res.write(chunk); }); proxyRes.on('end', () => { res.end(); // Try to parse response and record usage try { const responseData = JSON.parse(responseBody); if (responseData.usage) { recordUsage(alias, requestData, responseData); } } catch (e) { // Response might be streaming or non-JSON } }); // Copy response headers res.writeHead(proxyRes.statusCode, proxyRes.headers); }); proxyReq.on('error', (error) => { console.error('Proxy error:', error.message); res.writeHead(502); res.end(JSON.stringify({ error: 'Proxy error', message: error.message })); }); if (body) { proxyReq.write(body); } proxyReq.end(); }); } /** * Handle streaming responses (SSE) * @param {Object} req - Incoming request * @param {Object} res - Outgoing response * @param {string} alias - API key alias */ function proxyStreamingRequest(req, res, alias) { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { let requestData = {}; try { if (body) { requestData = JSON.parse(body); } } catch (e) { // Not JSON } const targetPath = req.url.replace(/^\/anthropic/, ''); const options = { hostname: DEEPSEEK_HOST, port: 443, path: '/anthropic' + targetPath, method: req.method, headers: { ...req.headers, host: DEEPSEEK_HOST } }; delete options.headers['content-length']; if (body) { options.headers['content-length'] = Buffer.byteLength(body); } const proxyReq = https.request(options, (proxyRes) => { // Set headers for SSE res.writeHead(proxyRes.statusCode, { ...proxyRes.headers, 'cache-control': 'no-cache', 'connection': 'keep-alive' }); let totalInputTokens = 0; let totalOutputTokens = 0; proxyRes.on('data', chunk => { res.write(chunk); // Try to extract usage from SSE events const lines = chunk.toString().split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { try { const data = JSON.parse(line.slice(6)); if (data.usage) { totalInputTokens = data.usage.input_tokens || data.usage.prompt_tokens || totalInputTokens; totalOutputTokens = data.usage.output_tokens || data.usage.completion_tokens || totalOutputTokens; } } catch (e) { // Not valid JSON } } } }); proxyRes.on('end', () => { res.end(); // Record final usage if we captured any if (totalInputTokens > 0 || totalOutputTokens > 0) { recordUsage(alias, requestData, { usage: { prompt_tokens: totalInputTokens, completion_tokens: totalOutputTokens } }); } }); }); proxyReq.on('error', (error) => { console.error('Proxy error:', error.message); res.writeHead(502); res.end(JSON.stringify({ error: 'Proxy error', message: error.message })); }); if (body) { proxyReq.write(body); } proxyReq.end(); }); } /** * Start the proxy server * @param {Object} options - Server options */ function startServer(options = {}) { const port = options.port || DEFAULT_PORT; const alias = options.alias || 'default'; const server = http.createServer((req, res) => { // Health check endpoint if (req.url === '/health') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'ok', alias })); return; } // Usage stats endpoint if (req.url === '/usage') { const data = loadUsageData(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(data, null, 2)); return; } // Usage stats for specific alias if (req.url.startsWith('/usage/')) { const requestedAlias = req.url.slice(7); const data = loadUsageData(); const aliasData = data.aliases[requestedAlias]; if (aliasData) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ alias: requestedAlias, ...aliasData }, null, 2)); } else { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Alias not found' })); } return; } // Check if streaming request const isStreaming = req.headers['accept']?.includes('text/event-stream') || req.url.includes('stream=true'); if (isStreaming) { proxyStreamingRequest(req, res, alias); } else { proxyRequest(req, res, alias); } }); server.listen(port, '127.0.0.1', () => { console.log(` ╔════════════════════════════════════════════════════════════╗ ║ DeepSeek Usage Tracker Proxy v1.0.0 ║ ╠════════════════════════════════════════════════════════════╣ ║ Proxy URL: http://127.0.0.1:${port}/anthropic ║ ║ Alias: ${alias.padEnd(43)}║ ║ Data File: ~/.aios/usage-tracking/deepseek-usage.json ║ ╠════════════════════════════════════════════════════════════╣ ║ Endpoints: ║ ║ /health - Health check ║ ║ /usage - All usage stats ║ ║ /usage/{alias} - Usage for specific alias ║ ╚════════════════════════════════════════════════════════════╝ `); }); return server; } /** * Get usage summary for CLI display * @param {string} alias - Optional alias to filter by * @returns {string} Formatted summary */ function getUsageSummary(alias = null) { const data = loadUsageData(); const lines = []; lines.push(''); lines.push('═'.repeat(60)); lines.push(' DeepSeek API Usage Summary'); lines.push('═'.repeat(60)); lines.push(''); if (alias && data.aliases[alias]) { const a = data.aliases[alias]; lines.push(` Alias: ${alias}`); lines.push(` ─${'─'.repeat(56)}`); lines.push(` Requests: ${a.requests.toLocaleString()}`); lines.push(` Input Tokens: ${a.tokens.input.toLocaleString()}`); lines.push(` Output Tokens: ${a.tokens.output.toLocaleString()}`); lines.push(` Total Cost: $${a.cost.toFixed(4)}`); lines.push(''); if (Object.keys(a.byModel).length > 0) { lines.push(' By Model:'); for (const [model, stats] of Object.entries(a.byModel)) { lines.push(` ${model}: ${stats.requests} req, $${stats.cost.toFixed(4)}`); } lines.push(''); } // Show last 7 days const days = Object.keys(a.byDay).sort().slice(-7); if (days.length > 0) { lines.push(' Last 7 Days:'); for (const day of days) { const d = a.byDay[day]; lines.push(` ${day}: ${d.requests} req, $${d.cost.toFixed(4)}`); } } } else { lines.push(` Total Requests: ${data.totalRequests.toLocaleString()}`); lines.push(` Total Tokens: ${(data.totalTokens.input + data.totalTokens.output).toLocaleString()}`); lines.push(` Total Cost: $${data.totalCost.toFixed(4)}`); lines.push(''); lines.push(' By Alias:'); lines.push(` ${'─'.repeat(56)}`); for (const [name, a] of Object.entries(data.aliases)) { lines.push(` ${name.padEnd(20)} ${a.requests.toString().padStart(8)} req $${a.cost.toFixed(4)}`); } } lines.push(''); lines.push('═'.repeat(60)); lines.push(` Data: ${USAGE_FILE}`); lines.push(` Updated: ${data.lastUpdated || 'Never'}`); lines.push('═'.repeat(60)); lines.push(''); return lines.join('\n'); } // CLI interface if (require.main === module) { const args = process.argv.slice(2); const command = args[0]; switch (command) { case 'start': case 'serve': { const port = parseInt(args.find(a => a.startsWith('--port='))?.split('=')[1]) || DEFAULT_PORT; const alias = args.find(a => a.startsWith('--alias='))?.split('=')[1] || 'claude-free'; startServer({ port, alias }); break; } case 'usage': case 'stats': { const alias = args[1]; console.log(getUsageSummary(alias)); break; } case 'reset': { if (fs.existsSync(USAGE_FILE)) { const backup = USAGE_FILE + '.backup.' + Date.now(); fs.copyFileSync(USAGE_FILE, backup); fs.unlinkSync(USAGE_FILE); console.log(`Usage data reset. Backup: ${backup}`); } else { console.log('No usage data to reset.'); } break; } case 'export': { const data = loadUsageData(); const exportFile = args[1] || `deepseek-usage-${new Date().toISOString().split('T')[0]}.json`; fs.writeFileSync(exportFile, JSON.stringify(data, null, 2)); console.log(`Usage data exported to: ${exportFile}`); break; } default: console.log(` DeepSeek Usage Tracker Commands: start [--port=8787] [--alias=claude-free] Start proxy server usage [alias] Show usage statistics reset Reset usage data (with backup) export [filename] Export usage data to JSON Examples: node index.js start --alias=claude-free node index.js usage claude-free node index.js export `); } } module.exports = { startServer, loadUsageData, saveUsageData, recordUsage, getUsageSummary, calculateCost, PRICING, DATA_DIR, USAGE_FILE };