Files
pdf-translation-interface/lib/cost-tracker.ts
aken1023 39a4788cc4 Add PDF translation API, utilities, docs, and config
Introduces core backend and frontend infrastructure for a PDF translation interface. Adds API endpoints for translation, PDF testing, and AI provider testing; implements PDF text extraction, cost tracking, and pricing logic in the lib directory; adds reusable UI components; and provides comprehensive documentation (SDD, environment setup, Claude instructions). Updates Tailwind and global styles, and includes a sample test PDF and configuration files.
2025-10-15 23:34:44 +08:00

229 lines
5.8 KiB
TypeScript

// Cost accumulation and tracking system
interface CostSession {
id: string
timestamp: number
model: string
provider: string
tokenUsage: {
promptTokens: number
completionTokens: number
totalTokens: number
}
cost: {
inputCost: number
outputCost: number
totalCost: number
currency: string
}
}
interface CostSummary {
totalSessions: number
totalTokens: number
totalCost: number
currency: string
startDate: number
lastUpdated: number
byProvider: Record<string, {
sessions: number
tokens: number
cost: number
}>
byModel: Record<string, {
sessions: number
tokens: number
cost: number
}>
}
class CostTracker {
private readonly STORAGE_KEY = 'pdf_translation_cost_tracker'
// Get stored cost summary
getCostSummary(): CostSummary {
if (typeof window === 'undefined') {
// Server-side fallback
return this.getDefaultSummary()
}
try {
const stored = localStorage.getItem(this.STORAGE_KEY)
if (!stored) {
return this.getDefaultSummary()
}
return JSON.parse(stored)
} catch (error) {
console.error('Error reading cost summary:', error)
return this.getDefaultSummary()
}
}
// Add new cost session
addCostSession(session: Omit<CostSession, 'id' | 'timestamp'>): CostSummary {
if (typeof window === 'undefined') {
// Server-side fallback
return this.getDefaultSummary()
}
const summary = this.getCostSummary()
const now = Date.now()
const newSession: CostSession = {
...session,
id: `session_${now}_${Math.random().toString(36).substr(2, 9)}`,
timestamp: now
}
// Update summary
summary.totalSessions += 1
summary.totalTokens += session.tokenUsage.totalTokens
summary.totalCost += session.cost.totalCost
summary.lastUpdated = now
if (summary.startDate === 0) {
summary.startDate = now
}
// Update by provider
if (!summary.byProvider[session.provider]) {
summary.byProvider[session.provider] = { sessions: 0, tokens: 0, cost: 0 }
}
summary.byProvider[session.provider].sessions += 1
summary.byProvider[session.provider].tokens += session.tokenUsage.totalTokens
summary.byProvider[session.provider].cost += session.cost.totalCost
// Update by model
if (!summary.byModel[session.model]) {
summary.byModel[session.model] = { sessions: 0, tokens: 0, cost: 0 }
}
summary.byModel[session.model].sessions += 1
summary.byModel[session.model].tokens += session.tokenUsage.totalTokens
summary.byModel[session.model].cost += session.cost.totalCost
// Save to localStorage
try {
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(summary))
} catch (error) {
console.error('Error saving cost summary:', error)
}
return summary
}
// Reset cost tracking
resetCostTracking(): CostSummary {
if (typeof window === 'undefined') {
return this.getDefaultSummary()
}
const summary = this.getDefaultSummary()
try {
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(summary))
} catch (error) {
console.error('Error resetting cost tracking:', error)
}
return summary
}
// Export cost data
exportCostData(): string {
const summary = this.getCostSummary()
return JSON.stringify(summary, null, 2)
}
// Format cost for display
formatCost(amount: number, currency: string = 'USD'): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency,
minimumFractionDigits: 4,
maximumFractionDigits: 4
}).format(amount)
}
// Format large numbers
formatNumber(num: number): string {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M'
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K'
}
return num.toString()
}
// Get cost insights
getCostInsights(): {
averageCostPerSession: number
averageTokensPerSession: number
mostUsedProvider: string
mostUsedModel: string
dailyAverage: number
} {
const summary = this.getCostSummary()
if (summary.totalSessions === 0) {
return {
averageCostPerSession: 0,
averageTokensPerSession: 0,
mostUsedProvider: '',
mostUsedModel: '',
dailyAverage: 0
}
}
// Calculate averages
const averageCostPerSession = summary.totalCost / summary.totalSessions
const averageTokensPerSession = summary.totalTokens / summary.totalSessions
// Find most used provider
let mostUsedProvider = ''
let maxProviderSessions = 0
Object.entries(summary.byProvider).forEach(([provider, stats]) => {
if (stats.sessions > maxProviderSessions) {
maxProviderSessions = stats.sessions
mostUsedProvider = provider
}
})
// Find most used model
let mostUsedModel = ''
let maxModelSessions = 0
Object.entries(summary.byModel).forEach(([model, stats]) => {
if (stats.sessions > maxModelSessions) {
maxModelSessions = stats.sessions
mostUsedModel = model
}
})
// Calculate daily average
const daysSinceStart = summary.startDate > 0
? Math.max(1, Math.ceil((Date.now() - summary.startDate) / (24 * 60 * 60 * 1000)))
: 1
const dailyAverage = summary.totalCost / daysSinceStart
return {
averageCostPerSession,
averageTokensPerSession,
mostUsedProvider,
mostUsedModel,
dailyAverage
}
}
private getDefaultSummary(): CostSummary {
return {
totalSessions: 0,
totalTokens: 0,
totalCost: 0,
currency: 'USD',
startDate: 0,
lastUpdated: 0,
byProvider: {},
byModel: {}
}
}
}
// Export singleton instance
export const costTracker = new CostTracker()
export type { CostSession, CostSummary }