diff --git a/src/app/dashboard/settings/pricing/page.js b/src/app/dashboard/settings/pricing/page.js new file mode 100644 index 0000000..ffcfe8d --- /dev/null +++ b/src/app/dashboard/settings/pricing/page.js @@ -0,0 +1,173 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import Card from "@/shared/components/Card"; +import PricingModal from "@/shared/components/PricingModal"; + +export default function PricingSettingsPage() { + const router = useRouter(); + const [showModal, setShowModal] = useState(false); + const [currentPricing, setCurrentPricing] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadPricing(); + }, []); + + const loadPricing = async () => { + setLoading(true); + try { + const response = await fetch("/api/pricing"); + if (response.ok) { + const data = await response.json(); + setCurrentPricing(data); + } + } catch (error) { + console.error("Failed to load pricing:", error); + } finally { + setLoading(false); + } + }; + + const handlePricingUpdated = () => { + loadPricing(); + }; + + // Count total models with pricing + const getModelCount = () => { + if (!currentPricing) return 0; + let count = 0; + for (const provider in currentPricing) { + count += Object.keys(currentPricing[provider]).length; + } + return count; + }; + + // Get providers list + const getProviders = () => { + if (!currentPricing) return []; + return Object.keys(currentPricing).sort(); + }; + + return ( +
+ {/* Header */} +
+
+

Pricing Settings

+

+ Configure pricing rates for cost tracking and calculations +

+
+ +
+ + {/* Quick Stats */} +
+ +
+ Total Models +
+
+ {loading ? "..." : getModelCount()} +
+
+ +
+ Providers +
+
+ {loading ? "..." : getProviders().length} +
+
+ +
+ Status +
+
+ {loading ? "..." : "Active"} +
+
+
+ + {/* Info Section */} + +

How Pricing Works

+
+

+ Cost Calculation: Costs are calculated based on token usage and pricing rates. + Each request's cost is determined by: (input_tokens × input_rate) + (output_tokens × output_rate) + (cached_tokens × cached_rate) +

+

+ Pricing Format: All rates are in dollars per million tokens ($/1M tokens). + Example: An input rate of 2.50 means $2.50 per 1,000,000 input tokens. +

+

+ Token Types: +

+
    +
  • Input: Standard prompt tokens
  • +
  • Output: Completion/response tokens
  • +
  • Cached: Cached input tokens (typically 50% of input rate)
  • +
  • Reasoning: Special reasoning/thinking tokens (fallback to output rate)
  • +
  • Cache Creation: Tokens used to create cache entries (fallback to input rate)
  • +
+

+ Custom Pricing: You can override default pricing for specific models. + Reset to defaults anytime to restore standard rates. +

+
+
+ + {/* Current Pricing Preview */} + +
+

Current Pricing Overview

+ +
+ + {loading ? ( +
Loading pricing data...
+ ) : currentPricing ? ( +
+ {Object.keys(currentPricing).slice(0, 5).map(provider => ( +
+ {provider.toUpperCase()}:{" "} + + {Object.keys(currentPricing[provider]).length} models + +
+ ))} + {Object.keys(currentPricing).length > 5 && ( +
+ + {Object.keys(currentPricing).length - 5} more providers +
+ )} +
+ ) : ( +
No pricing data available
+ )} +
+ + {/* Pricing Modal */} + {showModal && ( + setShowModal(false)} + onSave={handlePricingUpdated} + /> + )} +
+ ); +} \ No newline at end of file diff --git a/src/shared/components/PricingModal.js b/src/shared/components/PricingModal.js new file mode 100644 index 0000000..ad21725 --- /dev/null +++ b/src/shared/components/PricingModal.js @@ -0,0 +1,208 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { getDefaultPricing, formatCost } from "@/shared/constants/pricing.js"; + +export default function PricingModal({ isOpen, onClose, onSave }) { + const [pricingData, setPricingData] = useState({}); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (isOpen) { + loadPricing(); + } + }, [isOpen]); + + const loadPricing = async () => { + setLoading(true); + try { + const response = await fetch("/api/pricing"); + if (response.ok) { + const data = await response.json(); + setPricingData(data); + } else { + // Fallback to defaults + const defaults = getDefaultPricing(); + setPricingData(defaults); + } + } catch (error) { + console.error("Failed to load pricing:", error); + const defaults = getDefaultPricing(); + setPricingData(defaults); + } finally { + setLoading(false); + } + }; + + const handlePricingChange = (provider, model, field, value) => { + const numValue = parseFloat(value); + if (isNaN(numValue) || numValue < 0) return; + + setPricingData(prev => { + const newData = { ...prev }; + if (!newData[provider]) newData[provider] = {}; + if (!newData[provider][model]) newData[provider][model] = {}; + newData[provider][model][field] = numValue; + return newData; + }); + }; + + const handleSave = async () => { + setSaving(true); + try { + const response = await fetch("/api/pricing", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(pricingData) + }); + + if (response.ok) { + onSave?.(); + onClose(); + } else { + const error = await response.json(); + alert(`Failed to save pricing: ${error.error}`); + } + } catch (error) { + console.error("Failed to save pricing:", error); + alert("Failed to save pricing"); + } finally { + setSaving(false); + } + }; + + const handleReset = async () => { + if (!confirm("Reset all pricing to defaults? This cannot be undone.")) return; + + try { + const response = await fetch("/api/pricing", { method: "DELETE" }); + if (response.ok) { + const defaults = getDefaultPricing(); + setPricingData(defaults); + } + } catch (error) { + console.error("Failed to reset pricing:", error); + alert("Failed to reset pricing"); + } + }; + + if (!isOpen) return null; + + // Get all unique providers and models for display + const allProviders = Object.keys(pricingData).sort(); + const pricingFields = ["input", "output", "cached", "reasoning", "cache_creation"]; + + return ( +
+
+ {/* Header */} +
+

Pricing Configuration

+ +
+ + {/* Content */} +
+ {loading ? ( +
Loading pricing data...
+ ) : ( +
+ {/* Instructions */} +
+

Pricing Rates Format

+

+ All rates are in dollars per million tokens ($/1M tokens). + Example: Input rate of 2.50 means $2.50 per 1,000,000 input tokens. +

+
+ + {/* Pricing Tables */} + {allProviders.map(provider => { + const models = Object.keys(pricingData[provider]).sort(); + return ( +
+
+ {provider.toUpperCase()} +
+
+ + + + + + + + + + + + + {models.map(model => ( + + + {pricingFields.map(field => ( + + ))} + + ))} + +
ModelInputOutputCachedReasoningCache Creation
{model} + handlePricingChange(provider, model, field, e.target.value)} + className="w-20 px-2 py-1 text-right bg-bg-base border border-border rounded focus:outline-none focus:border-primary" + /> +
+
+
+ ); + })} + + {allProviders.length === 0 && ( +
+ No pricing data available +
+ )} +
+ )} +
+ + {/* Footer */} +
+ +
+ + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/shared/components/UsageStats.js b/src/shared/components/UsageStats.js index 76c2686..d2a8525 100644 --- a/src/shared/components/UsageStats.js +++ b/src/shared/components/UsageStats.js @@ -37,6 +37,7 @@ export default function UsageStats() { const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [autoRefresh, setAutoRefresh] = useState(true); + const [viewMode, setViewMode] = useState("tokens"); // 'tokens' or 'costs' const toggleSort = (field) => { const params = new URLSearchParams(searchParams.toString()); @@ -51,12 +52,31 @@ export default function UsageStats() { const sortData = (dataMap, pendingMap = {}) => { return Object.entries(dataMap || {}) - .map(([key, data]) => ({ - ...data, - key, - totalTokens: (data.promptTokens || 0) + (data.completionTokens || 0), - pending: pendingMap[key] || 0, - })) + .map(([key, data]) => { + const totalTokens = + (data.promptTokens || 0) + (data.completionTokens || 0); + const totalCost = data.cost || 0; + + // Calculate cost breakdown (estimated based on token ratio) + const inputCost = + totalTokens > 0 + ? (data.promptTokens || 0) * (totalCost / totalTokens) + : 0; + const outputCost = + totalTokens > 0 + ? (data.completionTokens || 0) * (totalCost / totalTokens) + : 0; + + return { + ...data, + key, + totalTokens, + totalCost, + inputCost, + outputCost, + pending: pendingMap[key] || 0, + }; + }) .sort((a, b) => { let valA = a[sortBy]; let valB = b[sortBy]; @@ -133,6 +153,9 @@ export default function UsageStats() { // Format number with commas const fmt = (n) => new Intl.NumberFormat().format(n || 0); + // Format cost with dollar sign and 2 decimals + const fmtCost = (n) => `$${(n || 0).toFixed(2)}`; + // Time format for "Last Used" const fmtTime = (iso) => { if (!iso) return "Never"; @@ -149,10 +172,35 @@ export default function UsageStats() { return (
- {/* Header with Auto Refresh Toggle */} + {/* Header with Auto Refresh Toggle and View Toggle */}

Usage Overview

+ {/* View Toggle */} +
+ + +
+ + {/* Auto Refresh Toggle */}