termcode
Version:
Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative
179 lines (178 loc) • 7.22 kB
JavaScript
import { getProvider } from "../providers/index.js";
import { loadConfig } from "../state/config.js";
import { updateSession } from "../state/session.js";
import { log } from "./logging.js";
// Simple token estimation for different content types
export function estimateTokens(text, model = "gpt-4") {
// Rough approximation: 1 token ≈ 0.75 words ≈ 4 characters
// More accurate for English text, less for code
const chars = text.length;
const baseTokens = Math.ceil(chars / 4);
// Adjust based on model type
if (model.includes("gpt-4") || model.includes("claude")) {
return Math.ceil(baseTokens * 1.1); // Slightly higher for advanced models
}
else if (model.includes("code") || model.includes("codellama")) {
return Math.ceil(baseTokens * 1.2); // Code models tend to use more tokens
}
return baseTokens;
}
export async function calculateCost(provider, model, inputTokens, outputTokens = 0) {
try {
const providerInstance = getProvider(provider);
if (providerInstance.estimateCost) {
const inputCost = providerInstance.estimateCost(inputTokens, model, "chat");
const outputCost = outputTokens > 0 ?
providerInstance.estimateCost(outputTokens, model, "chat") : 0;
return inputCost + outputCost;
}
// Fallback to generic pricing
return getGenericCost(provider, model, inputTokens + outputTokens);
}
catch (error) {
log.warn(`Failed to calculate cost for ${provider}/${model}, using fallback`);
return getGenericCost(provider, model, inputTokens + outputTokens);
}
}
function getGenericCost(provider, model, tokens) {
// Fallback pricing per 1K tokens (USD)
const genericPricing = {
openai: {
"gpt-4o": 0.005,
"gpt-4o-mini": 0.00015,
"gpt-4": 0.03,
"gpt-3.5-turbo": 0.001,
},
anthropic: {
"claude-3-5-sonnet": 0.003,
"claude-3-haiku": 0.00025,
"claude-3-opus": 0.015,
},
google: {
"gemini-pro": 0.001,
"gemini-1.5-pro": 0.0035,
},
xai: {
"grok-beta": 0.005,
},
mistral: {
"mistral-large": 0.008,
"mistral-medium": 0.0027,
},
cohere: {
"command": 0.001,
"command-r-plus": 0.003,
},
ollama: {
// Local models have no API cost
default: 0,
}
};
const providerPricing = genericPricing[provider];
if (!providerPricing)
return 0;
const price = providerPricing[model] || providerPricing.default || 0.002;
return (tokens / 1000) * price;
}
export async function recordUsage(provider, model, task, inputTokens, outputTokens = 0, repoPath) {
const totalTokens = inputTokens + outputTokens;
const costUSD = await calculateCost(provider, model, inputTokens, outputTokens);
const record = {
timestamp: new Date().toISOString(),
provider,
model,
task: task.substring(0, 100), // Truncate long tasks
inputTokens,
outputTokens,
totalTokens,
costUSD,
repoPath
};
// Update session if repo path provided
if (repoPath) {
await updateSession(repoPath, {
totalTokensUsed: totalTokens,
totalCostUSD: costUSD
});
}
// Check budget and warn if needed
await checkBudgetAndWarn(costUSD);
return record;
}
export async function getBudgetStatus() {
const config = await loadConfig();
const monthlyBudget = config?.routing?.budgetUSDMonthly || 10;
// Get current month's spending
// For now, we'll use a simple approximation based on session data
// In a real implementation, you'd want to persist usage records
const currentSpent = await getCurrentMonthSpending();
const remaining = Math.max(0, monthlyBudget - currentSpent);
const percentage = (currentSpent / monthlyBudget) * 100;
const isOverBudget = currentSpent > monthlyBudget;
// Calculate days into current month
const now = new Date();
const daysIntoMonth = now.getDate();
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
// Project monthly spend based on current usage
const dailyAverage = currentSpent / daysIntoMonth;
const projectedMonthlySpend = dailyAverage * daysInMonth;
return {
monthlyBudget,
currentSpent,
remaining,
percentage,
isOverBudget,
daysIntoMonth,
projectedMonthlySpend
};
}
async function getCurrentMonthSpending() {
// This would ideally read from a persistent usage log
// For now, we'll return 0 as a placeholder
// In a real implementation, you'd want to:
// 1. Store usage records in ~/.termcode/usage/YYYY-MM.json
// 2. Read and sum costs for current month
return 0;
}
async function checkBudgetAndWarn(newCost) {
const status = await getBudgetStatus();
const newTotal = status.currentSpent + newCost;
const newPercentage = (newTotal / status.monthlyBudget) * 100;
// Warn at different thresholds
if (newPercentage >= 100 && status.percentage < 100) {
log.error(`🚨 Monthly budget exceeded! Spent $${newTotal.toFixed(4)} of $${status.monthlyBudget}`);
log.warn("Consider upgrading your budget in /config or using cheaper models");
}
else if (newPercentage >= 90 && status.percentage < 90) {
log.warn(`⚠️ Approaching budget limit: $${newTotal.toFixed(4)} of $${status.monthlyBudget} (${newPercentage.toFixed(1)}%)`);
}
else if (newPercentage >= 75 && status.percentage < 75) {
log.warn(`💰 Budget notice: $${newTotal.toFixed(4)} of $${status.monthlyBudget} used (${newPercentage.toFixed(1)}%)`);
}
}
export async function formatBudgetStatus() {
const status = await getBudgetStatus();
const spentFormatted = `$${status.currentSpent.toFixed(4)}`;
const budgetFormatted = `$${status.monthlyBudget}`;
const remainingFormatted = `$${status.remaining.toFixed(4)}`;
const percentageFormatted = `${status.percentage.toFixed(1)}%`;
let statusIcon = "💚";
if (status.isOverBudget)
statusIcon = "🚨";
else if (status.percentage >= 90)
statusIcon = "⚠️";
else if (status.percentage >= 75)
statusIcon = "💛";
let output = `${statusIcon} Budget: ${spentFormatted} of ${budgetFormatted} used (${percentageFormatted})`;
if (status.projectedMonthlySpend > status.monthlyBudget * 1.1) {
const projectedFormatted = `$${status.projectedMonthlySpend.toFixed(2)}`;
output += `\\n 📈 Projected monthly spend: ${projectedFormatted}`;
}
return output;
}
// Utility function for the agent to use when processing tasks
export async function trackTaskUsage(provider, model, task, inputText, outputText = "", repoPath) {
const inputTokens = estimateTokens(inputText, model);
const outputTokens = outputText ? estimateTokens(outputText, model) : 0;
await recordUsage(provider, model, task, inputTokens, outputTokens, repoPath);
}