diff --git a/src/api/controller/stats.js b/src/api/controller/stats.js new file mode 100644 index 0000000..46064b8 --- /dev/null +++ b/src/api/controller/stats.js @@ -0,0 +1,149 @@ +const Prompt = require('../../models/Prompt'); +const { Op } = require('sequelize'); +const sequelize = require('../../config/database'); + +/** + * Calculate expense summaries per model with time granularity + * @param {Object} [options] - Query options + * @param {string} [options.startDate] - Start date for the summary (ISO format) + * @param {string} [options.endDate] - End date for the summary (ISO format) + * @param {string} [options.granularity='monthly'] - Time granularity ('daily', 'monthly', 'yearly') + * @returns {Promise} Summary of expenses per model and time period + */ +async function getModelExpenses({ startDate, endDate, granularity = 'monthly' } = {}) { + const where = {}; + + // Add date range if provided + if (startDate || endDate) { + where.created_at = {}; + if (startDate) where.created_at[Op.gte] = new Date(startDate); + if (endDate) where.created_at[Op.lte] = new Date(endDate); + } + + // Define time period format based on granularity + const timeFormat = { + daily: '%Y-%m-%d', + monthly: '%Y-%m', + yearly: '%Y' + }[granularity] || '%Y-%m'; // Default to monthly if invalid granularity + + // Get all prompts within the date range + const prompts = await Prompt.findAll({ + where, + attributes: [ + 'model', + [sequelize.fn('date_trunc', granularity, sequelize.col('created_at')), 'time_period'], + [sequelize.fn('COUNT', sequelize.col('id')), 'total_requests'], + [sequelize.fn('SUM', sequelize.col('prompt_tokens')), 'total_prompt_tokens'], + [sequelize.fn('SUM', sequelize.col('response_tokens')), 'total_response_tokens'], + [sequelize.fn('SUM', sequelize.col('total_tokens')), 'total_tokens'], + [sequelize.fn('SUM', sequelize.col('your_cost_usd')), 'total_cost'], + [sequelize.fn('AVG', sequelize.col('your_cost_usd')), 'avg_cost_per_request'], + [sequelize.fn('SUM', sequelize.col('cost_gpt4_usd')), 'total_gpt4_cost'], + [sequelize.fn('SUM', sequelize.col('cost_claude_usd')), 'total_claude_cost'], + [sequelize.fn('SUM', sequelize.col('cost_gemini_usd')), 'total_gemini_cost'], + [sequelize.fn('SUM', sequelize.col('savings_vs_gpt4')), 'total_savings_vs_gpt4'], + [sequelize.fn('SUM', sequelize.col('savings_vs_claude')), 'total_savings_vs_claude'], + [sequelize.fn('SUM', sequelize.col('savings_vs_gemini')), 'total_savings_vs_gemini'] + ], + group: ['model', 'time_period'], + order: [ + ['time_period', 'DESC'], + [sequelize.fn('SUM', sequelize.col('your_cost_usd')), 'DESC'] + ] + }); + + // Group results by time period + const groupedByTime = prompts.reduce((acc, model) => { + const timePeriod = model.getDataValue('time_period'); + const modelData = { + model: model.getDataValue('model'), + total_requests: parseInt(model.getDataValue('total_requests')), + total_prompt_tokens: parseInt(model.getDataValue('total_prompt_tokens') || 0), + total_response_tokens: parseInt(model.getDataValue('total_response_tokens') || 0), + total_tokens: parseInt(model.getDataValue('total_tokens') || 0), + total_cost: parseFloat(model.getDataValue('total_cost') || 0).toFixed(6), + avg_cost_per_request: parseFloat(model.getDataValue('avg_cost_per_request') || 0).toFixed(6), + total_gpt4_cost: parseFloat(model.getDataValue('total_gpt4_cost') || 0).toFixed(6), + total_claude_cost: parseFloat(model.getDataValue('total_claude_cost') || 0).toFixed(6), + total_gemini_cost: parseFloat(model.getDataValue('total_gemini_cost') || 0).toFixed(6), + total_savings_vs_gpt4: parseFloat(model.getDataValue('total_savings_vs_gpt4') || 0).toFixed(6), + total_savings_vs_claude: parseFloat(model.getDataValue('total_savings_vs_claude') || 0).toFixed(6), + total_savings_vs_gemini: parseFloat(model.getDataValue('total_savings_vs_gemini') || 0).toFixed(6) + }; + + if (!acc[timePeriod]) { + acc[timePeriod] = { + models: [], + totals: { + total_requests: 0, + total_prompt_tokens: 0, + total_response_tokens: 0, + total_tokens: 0, + total_cost: 0, + total_gpt4_cost: 0, + total_claude_cost: 0, + total_gemini_cost: 0, + total_savings_vs_gpt4: 0, + total_savings_vs_claude: 0, + total_savings_vs_gemini: 0 + } + }; + } + + acc[timePeriod].models.push(modelData); + + // Update period totals + const totals = acc[timePeriod].totals; + totals.total_requests += modelData.total_requests; + totals.total_prompt_tokens += modelData.total_prompt_tokens; + totals.total_response_tokens += modelData.total_response_tokens; + totals.total_tokens += modelData.total_tokens; + totals.total_cost += parseFloat(modelData.total_cost); + totals.total_gpt4_cost += parseFloat(modelData.total_gpt4_cost); + totals.total_claude_cost += parseFloat(modelData.total_claude_cost); + totals.total_gemini_cost += parseFloat(modelData.total_gemini_cost); + totals.total_savings_vs_gpt4 += parseFloat(modelData.total_savings_vs_gpt4); + totals.total_savings_vs_claude += parseFloat(modelData.total_savings_vs_claude); + totals.total_savings_vs_gemini += parseFloat(modelData.total_savings_vs_gemini); + + return acc; + }, {}); + + // Format the response + const periods = Object.entries(groupedByTime).map(([timePeriod, data]) => { + const totals = data.totals; + totals.avg_cost_per_request = totals.total_requests > 0 + ? (totals.total_cost / totals.total_requests).toFixed(6) + : '0.000000'; + + // Format all totals to 6 decimal places + Object.keys(totals).forEach(key => { + if (typeof totals[key] === 'number') { + totals[key] = totals[key].toFixed(6); + } + }); + + return { + time_period: timePeriod, + models: data.models, + totals + }; + }); + + // Sort periods by time (newest first) + periods.sort((a, b) => new Date(b.time_period) - new Date(a.time_period)); + + return { + granularity, + periods, + date_range: { + start: startDate || null, + end: endDate || null + } + }; +} + +module.exports = { + getModelExpenses +}; \ No newline at end of file diff --git a/src/api/network/stats.js b/src/api/network/stats.js new file mode 100644 index 0000000..bc6f601 --- /dev/null +++ b/src/api/network/stats.js @@ -0,0 +1,70 @@ +const { getModelExpenses } = require('../controller/stats'); + +/** + * Get expense summaries per model + * @param {Object} req - Express request object + * @param {Object} res - Express response object + */ +async function getExpenses(req, res) { + try { + const { start_date, end_date, granularity = 'monthly' } = req.query; + + // Validate granularity + if (!['daily', 'monthly', 'yearly'].includes(granularity)) { + return res.status(400).json({ + success: false, + error: 'Invalid granularity. Must be one of: daily, monthly, yearly' + }); + } + + // Validate date formats if provided + if (start_date && !isValidDate(start_date)) { + return res.status(400).json({ + success: false, + error: 'Invalid start_date format. Use ISO 8601 format (YYYY-MM-DD)' + }); + } + + if (end_date && !isValidDate(end_date)) { + return res.status(400).json({ + success: false, + error: 'Invalid end_date format. Use ISO 8601 format (YYYY-MM-DD)' + }); + } + + const summary = await getModelExpenses({ + startDate: start_date, + endDate: end_date, + granularity + }); + + res.json({ + success: true, + data: summary + }); + } catch (error) { + console.error('Error fetching expense summary:', error); + res.status(500).json({ + success: false, + error: 'Failed to fetch expense summary', + message: error.message + }); + } +} + +/** + * Validate date string format (YYYY-MM-DD) + * @param {string} dateString - The date string to validate + * @returns {boolean} Whether the date string is valid + */ +function isValidDate(dateString) { + const regex = /^\d{4}-\d{2}-\d{2}$/; + if (!regex.test(dateString)) return false; + + const date = new Date(dateString); + return date instanceof Date && !isNaN(date); +} + +module.exports = { + getExpenses +}; \ No newline at end of file diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..94b7aae --- /dev/null +++ b/src/app.js @@ -0,0 +1,8 @@ +const generateRoutes = require('./routes/generate'); +const logsRoutes = require('./routes/logs'); +const statsRoutes = require('./routes/stats'); + +// API routes +app.use('/api/generate', generateRoutes); +app.use('/api/logs', logsRoutes); +app.use('/api/stats', statsRoutes); \ No newline at end of file diff --git a/src/routes/stats.js b/src/routes/stats.js new file mode 100644 index 0000000..7fd45a7 --- /dev/null +++ b/src/routes/stats.js @@ -0,0 +1,16 @@ +const express = require('express'); +const router = express.Router(); +const { validateApiKey } = require('../middleware/auth'); +const { getExpenses } = require('../api/network/stats'); + +// Apply API key validation to all routes +router.use(validateApiKey); + +// Get expense summaries per model +// Optional query parameters: +// - start_date: Start date in YYYY-MM-DD format +// - end_date: End date in YYYY-MM-DD format +// - granularity: Time granularity ('daily', 'monthly', 'yearly'). Defaults to 'monthly' +router.get('/expenses', getExpenses); + +module.exports = router; \ No newline at end of file