Cost Tracking
Learn how to monitor, analyze, and optimize LLM spending in production with Mindwave's built-in cost tracking.
Overview
LLM costs can quickly add up in production applications. A single high-traffic endpoint making ChatGPT calls can cost hundreds or thousands of dollars per month. Mindwave provides automatic, real-time cost tracking to help you:
- Monitor spending as it happens, not days later on your invoice
- Attribute costs by user, feature, endpoint, or any custom dimension
- Set budget alerts to prevent unexpected spending spikes
- Optimize prompts based on actual cost/performance data
- Compare providers with real numbers from your application
- Build cost dashboards using traces stored in your database
Why Track Costs in Production?
Traditional approach:
- Wait for monthly invoice
- Guess which features drove costs
- React to overspending after it happens
Mindwave approach:
- Real-time cost visibility on every LLM call
- Cost data attached to every trace
- Proactive alerts and budgets
- Data-driven optimization decisions
How Cost Tracking Works
Mindwave automatically calculates costs for every LLM operation using a simple but accurate formula:
Token Counting
Every LLM provider returns token usage in their API responses:
{
"usage": {
"prompt_tokens": 150, // Input tokens
"completion_tokens": 300, // Output tokens
"total_tokens": 450
}
}Mindwave captures this data automatically through the GenAiInstrumentor and stores it in trace attributes:
gen_ai.usage.input_tokensgen_ai.usage.output_tokensgen_ai.usage.total_tokensgen_ai.usage.cache_read_tokens(Anthropic Claude)gen_ai.usage.cache_creation_tokens(Anthropic Claude)
Cost Calculation Formula
Cost is calculated using provider pricing tables:
Input Cost = (Input Tokens / 1000) × Price per 1000 Input Tokens
Output Cost = (Output Tokens / 1000) × Price per 1000 Output Tokens
Total Cost = Input Cost + Output CostExample calculation:
// GPT-4 pricing (as of 2025)
Input: $0.03 per 1K tokens
Output: $0.06 per 1K tokens
// Your LLM call used:
Input tokens: 1,500
Output tokens: 800
// Cost calculation:
Input cost: (1,500 / 1000) × $0.03 = $0.045
Output cost: (800 / 1000) × $0.06 = $0.048
Total cost: $0.045 + $0.048 = $0.093Integration with Tracing
Costs are automatically attached to spans in the estimated_cost field. This means:
- Every LLM call has its cost calculated automatically
- Stored in database for querying and analysis
- Aggregated at trace level for total request cost
- Available in real-time through events and logs
- Exported to OTLP backends for distributed analysis
Configuration
Model Pricing Tables
Mindwave ships with pricing for major LLM providers. Update these periodically as providers adjust their pricing:
// config/mindwave-tracing.php
'cost_estimation' => [
'enabled' => env('MINDWAVE_COST_ESTIMATION_ENABLED', true),
'pricing' => [
// OpenAI Pricing (per 1000 tokens, USD)
'openai' => [
'gpt-4' => [
'input' => 0.03,
'output' => 0.06,
],
'gpt-4-turbo' => [
'input' => 0.01,
'output' => 0.03,
],
'gpt-3.5-turbo' => [
'input' => 0.0005,
'output' => 0.0015,
],
],
// Anthropic Claude Pricing (per 1000 tokens, USD)
'anthropic' => [
'claude-3-opus' => [
'input' => 0.015,
'output' => 0.075,
],
'claude-3-sonnet' => [
'input' => 0.003,
'output' => 0.015,
],
'claude-3-haiku' => [
'input' => 0.00025,
'output' => 0.00125,
],
],
// Mistral AI Pricing (per 1000 tokens, USD)
'mistral' => [
'mistral-large-latest' => [
'input' => 0.004,
'output' => 0.012,
],
'mistral-medium-latest' => [
'input' => 0.0027,
'output' => 0.0081,
],
'mistral-small-latest' => [
'input' => 0.001,
'output' => 0.003,
],
],
// Google Gemini Pricing (per 1000 tokens, USD)
'google' => [
'gemini-pro' => [
'input' => 0.00025,
'output' => 0.0005,
],
],
],
],Environment Variables
Enable/disable cost estimation:
# Enable automatic cost calculation (default: true)
MINDWAVE_COST_ESTIMATION_ENABLED=trueCustom Pricing for Self-Hosted Models
If you're using self-hosted or custom models, add their pricing:
// config/mindwave-tracing.php
'pricing' => [
'custom-provider' => [
'my-model-v1' => [
'input' => 0.002, // $2 per million input tokens
'output' => 0.004, // $4 per million output tokens
],
],
// For self-hosted models with compute costs
'ollama' => [
'llama2' => [
'input' => 0.0, // Free inference
'output' => 0.0, // But track token usage anyway
],
],
],Model Version Handling
Mindwave automatically handles versioned model names:
// API returns: "gpt-4-0613"
// Mindwave looks up: "gpt-4-0613" → not found
// Falls back to: "gpt-4" → found!
// API returns: "claude-3-opus-20240229"
// Mindwave looks up: "claude-3-opus-20240229" → not found
// Falls back to: "claude-3-opus" → found!This means you don't need to configure every model version individually.
Accessing Cost Data
From LLM Response Events
Listen to the LlmResponseCompleted event to track costs in real-time:
use Mindwave\Mindwave\Observability\Events\LlmResponseCompleted;
use Illuminate\Support\Facades\Event;
Event::listen(LlmResponseCompleted::class, function (LlmResponseCompleted $event) {
// Check if cost estimate is available
if ($event->hasCostEstimate()) {
$cost = $event->costEstimate; // e.g., 0.0123 (USD)
$formatted = $event->getFormattedCost(); // e.g., "$0.0123"
echo "LLM call cost: {$formatted}\n";
echo "Input tokens: {$event->getInputTokens()}\n";
echo "Output tokens: {$event->getOutputTokens()}\n";
echo "Total tokens: {$event->getTotalTokens()}\n";
// Store in your application
UserLlmUsage::create([
'user_id' => auth()->id(),
'provider' => $event->provider,
'model' => $event->model,
'cost_usd' => $event->costEstimate,
'tokens' => $event->getTotalTokens(),
]);
}
});From Trace Database
Query costs directly from the mindwave_traces table:
use Mindwave\Mindwave\Observability\Models\Trace;
use Mindwave\Mindwave\Observability\Models\Span;
// Get today's total cost
$todayCost = Trace::whereDate('created_at', today())
->sum('estimated_cost');
echo "Today's LLM spend: $" . number_format($todayCost, 4);
// Get expensive traces (> $0.10)
$expensiveTraces = Trace::expensive(0.10)
->with('spans')
->get();
foreach ($expensiveTraces as $trace) {
echo "Trace: {$trace->trace_id}\n";
echo "Cost: $" . number_format($trace->estimated_cost, 4) . "\n";
echo "Duration: {$trace->getDurationInMilliseconds()}ms\n\n";
}From Individual Spans
Get per-operation cost details:
use Mindwave\Mindwave\Observability\Models\Span;
// Find most expensive LLM operations
$expensiveOps = Span::select('request_model', 'provider_name')
->selectRaw('COUNT(*) as call_count')
->selectRaw('SUM(input_tokens + output_tokens) as total_tokens')
->whereNotNull('request_model')
->groupBy('request_model', 'provider_name')
->orderByDesc('total_tokens')
->get();
foreach ($expensiveOps as $op) {
// Calculate cost manually from pricing config
$pricing = config("mindwave-tracing.cost_estimation.pricing.{$op->provider_name}.{$op->request_model}");
echo "Model: {$op->request_model}\n";
echo "Calls: {$op->call_count}\n";
echo "Total tokens: " . number_format($op->total_tokens) . "\n";
}Cost Attribution
Track costs by any dimension to understand where money is being spent.
Per-User Cost Tracking
Multi-tenant SaaS example:
use Mindwave\Mindwave\Observability\Events\LlmResponseCompleted;
Event::listen(LlmResponseCompleted::class, function (LlmResponseCompleted $event) {
if (!$event->hasCostEstimate()) {
return;
}
// Store per-user costs in your database
DB::table('user_llm_costs')->insert([
'user_id' => auth()->id(),
'tenant_id' => auth()->user()->tenant_id,
'cost_usd' => $event->costEstimate,
'provider' => $event->provider,
'model' => $event->model,
'tokens' => $event->getTotalTokens(),
'trace_id' => $event->traceId,
'created_at' => now(),
]);
});Query per-user spending:
// Get top spenders this month
$topSpenders = DB::table('user_llm_costs')
->select('user_id')
->selectRaw('SUM(cost_usd) as total_cost')
->selectRaw('SUM(tokens) as total_tokens')
->selectRaw('COUNT(*) as call_count')
->whereMonth('created_at', now()->month)
->groupBy('user_id')
->orderByDesc('total_cost')
->limit(10)
->get();
foreach ($topSpenders as $spender) {
$user = User::find($spender->user_id);
echo "{$user->name}: $" . number_format($spender->total_cost, 2) . "\n";
}Per-Feature Cost Tracking
Track which features consume the most LLM budget:
use Mindwave\Mindwave\Observability\Tracing\TracerManager;
class DocumentSummarizer
{
public function summarize(Document $document)
{
$tracer = app(TracerManager::class);
// Tag the trace with feature metadata
$span = $tracer->getCurrentSpan();
$span?->setAttribute('feature', 'document-summarization');
$span?->setAttribute('document_id', $document->id);
$span?->setAttribute('document_length', strlen($document->content));
$summary = Mindwave::llm()
->generateText("Summarize: {$document->content}");
return $summary;
}
}
// Query costs by feature
$featureCosts = DB::table('mindwave_spans')
->join('mindwave_traces', 'mindwave_spans.trace_id', '=', 'mindwave_traces.trace_id')
->select(DB::raw('JSON_UNQUOTE(JSON_EXTRACT(attributes, "$.feature")) as feature'))
->selectRaw('SUM(mindwave_traces.estimated_cost) as total_cost')
->selectRaw('COUNT(DISTINCT mindwave_traces.trace_id) as usage_count')
->whereNotNull(DB::raw('JSON_EXTRACT(attributes, "$.feature")'))
->groupBy('feature')
->orderByDesc('total_cost')
->get();Per-Request Cost Tracking
Track costs for individual API requests:
use Illuminate\Http\Request;
use Mindwave\Mindwave\Observability\Tracing\TracerManager;
class ApiController extends Controller
{
public function generateReport(Request $request)
{
$tracer = app(TracerManager::class);
// Create a root span for this API request
$span = $tracer->startSpan('api.generate-report', [
'http.method' => $request->method(),
'http.route' => $request->route()->getName(),
'api.version' => 'v1',
'client.id' => $request->header('X-Client-ID'),
]);
$scope = $span->activate();
try {
// Make multiple LLM calls (child spans)
$analysis = $this->analyzeData($request->data);
$summary = $this->generateSummary($analysis);
$recommendations = $this->generateRecommendations($summary);
$span->markAsOk();
return response()->json([
'analysis' => $analysis,
'summary' => $summary,
'recommendations' => $recommendations,
'metadata' => [
'trace_id' => $span->getTraceId(),
],
]);
} finally {
$scope->detach();
$span->end();
}
}
}
// Later, query total cost for this request
$trace = Trace::where('trace_id', $traceId)->first();
echo "Request cost: $" . number_format($trace->estimated_cost, 4);Cost Tagging with Metadata
Add custom metadata to traces for flexible cost analysis:
use Mindwave\Mindwave\Observability\Tracing\TracerManager;
class ChatbotService
{
public function handleMessage(string $message, User $user)
{
$tracer = app(TracerManager::class);
$span = $tracer->getCurrentSpan();
// Add rich metadata for cost analysis
$span?->setAttribute('app.feature', 'chatbot');
$span?->setAttribute('app.user_tier', $user->subscription_tier);
$span?->setAttribute('app.conversation_id', $user->current_conversation_id);
$span?->setAttribute('app.department', $user->department);
$span?->setAttribute('app.message_length', strlen($message));
$response = Mindwave::llm()->generateText($message);
return $response;
}
}
// Query: Which subscription tier costs the most?
$costByTier = DB::table('mindwave_spans')
->join('mindwave_traces', 'mindwave_spans.trace_id', '=', 'mindwave_traces.trace_id')
->select(DB::raw('JSON_UNQUOTE(JSON_EXTRACT(attributes, "$.app.user_tier")) as tier'))
->selectRaw('SUM(mindwave_traces.estimated_cost) as total_cost')
->whereNotNull(DB::raw('JSON_EXTRACT(attributes, "$.app.user_tier")'))
->groupBy('tier')
->get();Querying & Analysis
Using Database Queries
Daily cost report:
use Mindwave\Mindwave\Observability\Models\Trace;
$dailyCosts = Trace::selectRaw('DATE(created_at) as date')
->selectRaw('SUM(estimated_cost) as daily_cost')
->selectRaw('COUNT(*) as call_count')
->selectRaw('SUM(total_input_tokens) as input_tokens')
->selectRaw('SUM(total_output_tokens) as output_tokens')
->whereMonth('created_at', now()->month)
->groupBy('date')
->orderBy('date')
->get();
foreach ($dailyCosts as $day) {
echo "{$day->date}: $" . number_format($day->daily_cost, 2);
echo " ({$day->call_count} calls, " . number_format($day->input_tokens + $day->output_tokens) . " tokens)\n";
}Cost by provider comparison:
use Mindwave\Mindwave\Observability\Models\Span;
$providerComparison = Span::selectRaw('provider_name')
->selectRaw('COUNT(*) as call_count')
->selectRaw('SUM(input_tokens + output_tokens) as total_tokens')
->selectRaw('AVG(duration) as avg_duration')
->whereNotNull('provider_name')
->whereDate('created_at', '>=', now()->subDays(7))
->groupBy('provider_name')
->get();
// Calculate costs from tokens and pricing
foreach ($providerComparison as $provider) {
echo "\n{$provider->provider_name}:\n";
echo " Calls: " . number_format($provider->call_count) . "\n";
echo " Tokens: " . number_format($provider->total_tokens) . "\n";
echo " Avg Duration: " . round($provider->avg_duration / 1_000_000) . "ms\n";
}Cost per model with rankings:
$modelStats = DB::table('mindwave_spans')
->join('mindwave_traces', 'mindwave_spans.trace_id', '=', 'mindwave_traces.trace_id')
->select('request_model', 'provider_name')
->selectRaw('COUNT(*) as usage_count')
->selectRaw('AVG(mindwave_traces.estimated_cost) as avg_cost')
->selectRaw('SUM(input_tokens + output_tokens) as total_tokens')
->whereNotNull('request_model')
->groupBy('request_model', 'provider_name')
->orderByDesc('usage_count')
->get();
echo "\n📊 Model Usage & Cost Report\n";
echo str_repeat('=', 80) . "\n";
printf("%-30s %-10s %10s %15s\n", "Model", "Provider", "Uses", "Avg Cost");
echo str_repeat('-', 80) . "\n";
foreach ($modelStats as $stat) {
printf(
"%-30s %-10s %10s $%13s\n",
$stat->request_model,
$stat->provider_name,
number_format($stat->usage_count),
number_format($stat->avg_cost, 4)
);
}Using Artisan Commands
Mindwave includes a built-in command for cost analysis:
# View comprehensive statistics including costs
php artisan mindwave:trace-stats
# Filter by date range
php artisan mindwave:trace-stats --since=yesterday
# Filter by provider
php artisan mindwave:trace-stats --provider=openai
# Filter by model
php artisan mindwave:trace-stats --model=gpt-4Example output:
Mindwave Trace Statistics
Overall Statistics
+-----------------------+--------+
| Metric | Value |
+-----------------------+--------+
| Total Traces | 1,234 |
| Total Spans | 2,456 |
| Completed Traces | 1,230 |
| Avg Spans per Trace | 1.99 |
+-----------------------+--------+
Token Usage
+-----------------------+---------------+
| Metric | Value |
+-----------------------+---------------+
| Total Input Tokens | 1,234,567 |
| Total Output Tokens | 890,123 |
| Total Tokens | 2,124,690 |
| Avg Input Tokens | 1,000.46 |
| Avg Output Tokens | 721.33 |
+-----------------------+---------------+
Cost Analysis
+-----------------------+---------------+
| Metric | Value |
+-----------------------+---------------+
| Total Cost | $45.6789 |
| Average Cost | $0.0370 |
| Min Cost | $0.0001 |
| Max Cost | $2.3456 |
+-----------------------+---------------+Querying OpenTelemetry Backends
If you're exporting to Jaeger, Honeycomb, or Grafana:
Jaeger query examples:
# Find traces costing more than $0.50
service=my-app AND gen_ai.operation.name=chat
# Then filter by cost in the UI or programmaticallyHoneycomb query examples:
# Group by model and calculate average cost
BREAKDOWN: gen_ai.request.model
CALCULATE: AVG(gen_ai.usage.input_tokens), AVG(gen_ai.usage.output_tokens)
# Find expensive outliers
WHERE gen_ai.usage.total_tokens > P95(gen_ai.usage.total_tokens)Grafana Tempo query:
# Create a panel showing cost over time
{service.name="my-app"}
| gen_ai.operation.name="chat"Cost Optimization Strategies
Use cost data to make informed optimization decisions.
1. Model Selection Based on Cost/Performance
Compare models to find the best value:
use Mindwave\Mindwave\Observability\Models\Span;
// Get performance and cost metrics for each model
$modelComparison = Span::selectRaw('request_model')
->selectRaw('COUNT(*) as call_count')
->selectRaw('AVG(duration) as avg_duration_ns')
->selectRaw('AVG(input_tokens + output_tokens) as avg_tokens')
->whereIn('request_model', ['gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo'])
->whereDate('created_at', '>=', now()->subDays(7))
->groupBy('request_model')
->get();
foreach ($modelComparison as $model) {
$avgDurationMs = $model->avg_duration_ns / 1_000_000;
$pricing = config("mindwave-tracing.cost_estimation.pricing.openai.{$model->request_model}");
// Estimate average cost per call
$avgCost = ($model->avg_tokens / 2 / 1000 * $pricing['input']) +
($model->avg_tokens / 2 / 1000 * $pricing['output']);
echo "\n{$model->request_model}:\n";
echo " Calls: {$model->call_count}\n";
echo " Avg Duration: " . round($avgDurationMs) . "ms\n";
echo " Avg Cost: $" . number_format($avgCost, 4) . "\n";
echo " Cost per second: $" . number_format($avgCost / ($avgDurationMs / 1000), 4) . "\n";
}
// Decision: GPT-3.5-turbo might be 90% as good for 10% of the cost2. Prompt Engineering for Efficiency
Track how prompt changes affect costs:
class PromptOptimizer
{
public function testPromptVariants(array $prompts, string $testInput)
{
$results = [];
foreach ($prompts as $name => $prompt) {
$tracer = app(TracerManager::class);
$span = $tracer->startSpan("prompt-test-{$name}", [
'test.prompt_variant' => $name,
'test.prompt_length' => strlen($prompt),
]);
$startTime = microtime(true);
$response = Mindwave::llm()
->withSystemMessage($prompt)
->generateText($testInput);
$duration = microtime(true) - $startTime;
$span->end();
// Get cost from event
$cost = $this->getLastTraceCost($span->getTraceId());
$results[$name] = [
'response' => $response,
'cost' => $cost,
'duration' => $duration,
'prompt_length' => strlen($prompt),
];
}
return $results;
}
}
// Usage
$optimizer = new PromptOptimizer();
$results = $optimizer->testPromptVariants([
'verbose' => 'You are a helpful assistant. Please provide a detailed...',
'concise' => 'Summarize briefly:',
'structured' => 'Output format: JSON with keys: summary, key_points',
], $testDocument);
// Compare costs
foreach ($results as $name => $result) {
echo "{$name}: $" . number_format($result['cost'], 4) . " ({$result['prompt_length']} chars)\n";
}
// Pick the cheapest effective prompt3. Using PromptComposer to Reduce Token Usage
Mindwave's PromptComposer helps minimize input tokens:
use Mindwave\Mindwave\Prompt\PromptComposer;
// Before: Sending entire document (expensive)
$response = Mindwave::llm()
->generateText("Analyze this document: " . $largeDocument);
// After: Using composer to optimize (cheaper)
$prompt = PromptComposer::create()
->text('Analyze the following document sections:')
->section('Introduction', $introduction) // Only relevant sections
->section('Key Findings', $findings)
->maxTokens(2000) // Respect token limits
->build();
$response = Mindwave::llm()
->generateText($prompt);
// Track savings
$beforeCost = estimateCost(strlen($largeDocument));
$afterCost = estimateCost(strlen($prompt));
$savings = $beforeCost - $afterCost;
echo "Cost savings: $" . number_format($savings, 4) . " per request\n";4. Caching Strategies
Implement response caching to avoid repeated LLM calls:
use Illuminate\Support\Facades\Cache;
class CachedLlmService
{
public function generateWithCache(string $prompt, int $ttl = 3600)
{
$cacheKey = 'llm:' . md5($prompt);
return Cache::remember($cacheKey, $ttl, function () use ($prompt) {
// Track cache misses
app(TracerManager::class)
->getCurrentSpan()
?->setAttribute('cache.hit', false);
return Mindwave::llm()->generateText($prompt);
});
}
}
// Track cache effectiveness
$cacheHitRate = DB::table('mindwave_spans')
->whereNotNull(DB::raw('JSON_EXTRACT(attributes, "$.cache.hit")'))
->selectRaw('
SUM(CASE WHEN JSON_EXTRACT(attributes, "$.cache.hit") = true THEN 1 ELSE 0 END) as hits,
COUNT(*) as total
')
->first();
$hitRate = ($cacheHitRate->hits / $cacheHitRate->total) * 100;
echo "Cache hit rate: " . round($hitRate, 1) . "%\n";
// Calculate cost savings from caching
$avgCostPerCall = 0.05; // From your data
$savedCalls = $cacheHitRate->hits;
$savings = $savedCalls * $avgCostPerCall;
echo "Cost saved by caching: $" . number_format($savings, 2) . "\n";5. Batch Processing for Efficiency
Process multiple items in batches to reduce overhead:
class BatchProcessor
{
public function processBatch(array $items)
{
$tracer = app(TracerManager::class);
$span = $tracer->startSpan('batch-process', [
'batch.size' => count($items),
]);
// Combine items into single LLM call
$combinedPrompt = "Process these items:\n\n";
foreach ($items as $i => $item) {
$combinedPrompt .= ($i + 1) . ". {$item}\n";
}
$combinedPrompt .= "\nReturn results as numbered list.";
$response = Mindwave::llm()->generateText($combinedPrompt);
$span->end();
// Compare costs
$batchCost = $this->getTraceCost($span->getTraceId());
$individualCost = $this->estimateIndividualCost(count($items));
$savings = $individualCost - $batchCost;
echo "Batch cost: $" . number_format($batchCost, 4) . "\n";
echo "Individual cost would be: $" . number_format($individualCost, 4) . "\n";
echo "Savings: $" . number_format($savings, 4) . " (" . round($savings/$individualCost*100) . "%)\n";
return $this->parseResults($response);
}
}Real-World Examples
Multi-Tenant SaaS Cost Tracking
Complete example for tracking costs per tenant:
<?php
namespace App\Services;
use App\Models\Tenant;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Mindwave\Mindwave\Observability\Events\LlmResponseCompleted;
class TenantCostTracker
{
public function __construct()
{
// Listen to all LLM completions
Event::listen(LlmResponseCompleted::class, [$this, 'recordCost']);
}
public function recordCost(LlmResponseCompleted $event)
{
if (!$event->hasCostEstimate() || !auth()->check()) {
return;
}
$tenant = auth()->user()->tenant;
// Record in tenant costs table
DB::table('tenant_llm_costs')->insert([
'tenant_id' => $tenant->id,
'user_id' => auth()->id(),
'trace_id' => $event->traceId,
'span_id' => $event->spanId,
'provider' => $event->provider,
'model' => $event->model,
'cost_usd' => $event->costEstimate,
'input_tokens' => $event->getInputTokens(),
'output_tokens' => $event->getOutputTokens(),
'duration_ms' => $event->getDurationInMilliseconds(),
'created_at' => now(),
]);
// Update tenant running total
$tenant->increment('llm_cost_total', $event->costEstimate);
// Check budget limits
$this->checkBudgetLimit($tenant, $event->costEstimate);
}
protected function checkBudgetLimit(Tenant $tenant, float $cost)
{
$monthlySpend = $this->getMonthlySpend($tenant->id);
$budget = $tenant->llm_monthly_budget ?? 100.00;
if ($monthlySpend >= $budget) {
// Disable LLM features for this tenant
$tenant->update(['llm_enabled' => false]);
// Notify tenant admin
$tenant->owner->notify(new BudgetExceededNotification($monthlySpend, $budget));
\Log::warning("Tenant {$tenant->id} exceeded LLM budget", [
'tenant_id' => $tenant->id,
'monthly_spend' => $monthlySpend,
'budget' => $budget,
]);
} elseif ($monthlySpend >= $budget * 0.8) {
// Warning at 80%
$tenant->owner->notify(new BudgetWarningNotification($monthlySpend, $budget));
}
}
public function getMonthlySpend(int $tenantId): float
{
return DB::table('tenant_llm_costs')
->where('tenant_id', $tenantId)
->whereMonth('created_at', now()->month)
->whereYear('created_at', now()->year)
->sum('cost_usd');
}
public function getTenantReport(int $tenantId, int $days = 30): array
{
$costs = DB::table('tenant_llm_costs')
->where('tenant_id', $tenantId)
->where('created_at', '>=', now()->subDays($days))
->selectRaw('
DATE(created_at) as date,
provider,
model,
SUM(cost_usd) as daily_cost,
SUM(input_tokens + output_tokens) as daily_tokens,
COUNT(*) as call_count
')
->groupBy('date', 'provider', 'model')
->orderBy('date')
->get();
return [
'total_cost' => $costs->sum('daily_cost'),
'total_calls' => $costs->sum('call_count'),
'total_tokens' => $costs->sum('daily_tokens'),
'breakdown' => $costs,
];
}
}Tenant dashboard controller:
class TenantDashboardController extends Controller
{
public function llmCosts(TenantCostTracker $tracker)
{
$tenantId = auth()->user()->tenant_id;
// Get 30-day report
$report = $tracker->getTenantReport($tenantId, 30);
return view('tenant.llm-costs', [
'totalCost' => $report['total_cost'],
'totalCalls' => $report['total_calls'],
'totalTokens' => $report['total_tokens'],
'dailyBreakdown' => $report['breakdown'],
'monthlyBudget' => auth()->user()->tenant->llm_monthly_budget,
'percentUsed' => ($report['total_cost'] / auth()->user()->tenant->llm_monthly_budget) * 100,
]);
}
}Feature-Level Cost Allocation
Track costs by application feature:
<?php
namespace App\Services;
use Mindwave\Mindwave\Observability\Tracing\TracerManager;
class FeatureCostAllocator
{
protected array $features = [
'document-summarization',
'chatbot',
'content-generation',
'sentiment-analysis',
'translation',
];
public function trackFeature(string $feature, callable $callback)
{
if (!in_array($feature, $this->features)) {
throw new \InvalidArgumentException("Unknown feature: {$feature}");
}
$tracer = app(TracerManager::class);
$span = $tracer->startSpan("feature.{$feature}", [
'app.feature' => $feature,
'app.version' => config('app.version'),
]);
$scope = $span->activate();
try {
$result = $callback();
$span->markAsOk();
return $result;
} finally {
$scope->detach();
$span->end();
}
}
public function getFeatureCosts(int $days = 30): array
{
$costs = DB::table('mindwave_spans')
->join('mindwave_traces', 'mindwave_spans.trace_id', '=', 'mindwave_traces.trace_id')
->select(DB::raw('JSON_UNQUOTE(JSON_EXTRACT(mindwave_spans.attributes, "$.app.feature")) as feature'))
->selectRaw('SUM(mindwave_traces.estimated_cost) as total_cost')
->selectRaw('COUNT(DISTINCT mindwave_traces.trace_id) as usage_count')
->selectRaw('AVG(mindwave_traces.estimated_cost) as avg_cost')
->where('mindwave_spans.created_at', '>=', now()->subDays($days))
->whereNotNull(DB::raw('JSON_EXTRACT(mindwave_spans.attributes, "$.app.feature")'))
->groupBy('feature')
->orderByDesc('total_cost')
->get();
return $costs->mapWithKeys(fn($cost) => [
$cost->feature => [
'total' => $cost->total_cost,
'count' => $cost->usage_count,
'average' => $cost->avg_cost,
]
])->toArray();
}
}
// Usage
$allocator = new FeatureCostAllocator();
// In your application code
$allocator->trackFeature('document-summarization', function () use ($document) {
return Mindwave::llm()->generateText("Summarize: {$document->content}");
});
// Generate cost report
$featureCosts = $allocator->getFeatureCosts(30);
foreach ($featureCosts as $feature => $stats) {
echo "\n{$feature}:\n";
echo " Total cost: $" . number_format($stats['total'], 2) . "\n";
echo " Usage: {$stats['count']} times\n";
echo " Avg cost: $" . number_format($stats['average'], 4) . "\n";
}Budget Alerts and Limits
Implement proactive budget management:
<?php
namespace App\Services;
use App\Notifications\DailyBudgetExceeded;
use App\Notifications\WeeklyBudgetWarning;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Notification;
use Mindwave\Mindwave\Observability\Events\LlmResponseCompleted;
class BudgetManager
{
protected array $limits = [
'daily' => 10.00, // $10/day
'weekly' => 50.00, // $50/week
'monthly' => 200.00, // $200/month
];
public function checkBudget(LlmResponseCompleted $event)
{
if (!$event->hasCostEstimate()) {
return;
}
$this->checkDailyBudget($event->costEstimate);
$this->checkWeeklyBudget($event->costEstimate);
$this->checkMonthlyBudget($event->costEstimate);
}
protected function checkDailyBudget(float $cost)
{
$key = 'llm:budget:daily:' . now()->format('Y-m-d');
$spent = Cache::get($key, 0.0);
$newTotal = $spent + $cost;
Cache::put($key, $newTotal, now()->endOfDay());
if ($newTotal >= $this->limits['daily'] && $spent < $this->limits['daily']) {
// Just exceeded daily limit
Notification::route('slack', config('services.slack.webhook'))
->notify(new DailyBudgetExceeded($newTotal, $this->limits['daily']));
\Log::warning('Daily LLM budget exceeded', [
'spent' => $newTotal,
'limit' => $this->limits['daily'],
]);
}
}
protected function checkWeeklyBudget(float $cost)
{
$key = 'llm:budget:weekly:' . now()->format('Y-W');
$spent = Cache::get($key, 0.0);
$newTotal = $spent + $cost;
Cache::put($key, $newTotal, now()->endOfWeek());
// Warning at 80% of weekly budget
$warningThreshold = $this->limits['weekly'] * 0.8;
if ($newTotal >= $warningThreshold && $spent < $warningThreshold) {
Notification::route('slack', config('services.slack.webhook'))
->notify(new WeeklyBudgetWarning($newTotal, $this->limits['weekly']));
}
}
protected function checkMonthlyBudget(float $cost)
{
// Query actual database for accurate monthly total
$monthlySpent = DB::table('mindwave_traces')
->whereMonth('created_at', now()->month)
->whereYear('created_at', now()->year)
->sum('estimated_cost');
if ($monthlySpent >= $this->limits['monthly']) {
// Emergency: Disable LLM features
Cache::put('llm:disabled', true, now()->endOfMonth());
\Log::critical('Monthly LLM budget exceeded - LLM features disabled', [
'spent' => $monthlySpent,
'limit' => $this->limits['monthly'],
]);
// Send alert to admin
app(\App\Models\User::class)
->where('role', 'admin')
->first()
->notify(new MonthlyBudgetExceeded($monthlySpent, $this->limits['monthly']));
}
}
public function isLlmEnabled(): bool
{
return !Cache::get('llm:disabled', false);
}
public function getCurrentSpend(): array
{
return [
'daily' => Cache::get('llm:budget:daily:' . now()->format('Y-m-d'), 0.0),
'weekly' => Cache::get('llm:budget:weekly:' . now()->format('Y-W'), 0.0),
'monthly' => DB::table('mindwave_traces')
->whereMonth('created_at', now()->month)
->whereYear('created_at', now()->year)
->sum('estimated_cost'),
];
}
}
// Register in EventServiceProvider
Event::listen(LlmResponseCompleted::class, [BudgetManager::class, 'checkBudget']);Cost Reporting Dashboard
Build a comprehensive cost dashboard:
<?php
namespace App\Http\Controllers;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Mindwave\Mindwave\Observability\Models\Trace;
class LlmCostDashboardController extends Controller
{
public function index()
{
return view('admin.llm-costs', [
'summary' => $this->getSummary(),
'dailyTrend' => $this->getDailyTrend(),
'modelBreakdown' => $this->getModelBreakdown(),
'providerComparison' => $this->getProviderComparison(),
'topExpensiveTraces' => $this->getTopExpensiveTraces(),
'projections' => $this->getProjections(),
]);
}
protected function getSummary(): array
{
$today = Trace::whereDate('created_at', today())->sum('estimated_cost');
$yesterday = Trace::whereDate('created_at', today()->subDay())->sum('estimated_cost');
$thisMonth = Trace::whereMonth('created_at', now()->month)->sum('estimated_cost');
$lastMonth = Trace::whereMonth('created_at', now()->subMonth()->month)->sum('estimated_cost');
return [
'today' => $today,
'yesterday' => $yesterday,
'change_daily' => $yesterday > 0 ? (($today - $yesterday) / $yesterday) * 100 : 0,
'this_month' => $thisMonth,
'last_month' => $lastMonth,
'change_monthly' => $lastMonth > 0 ? (($thisMonth - $lastMonth) / $lastMonth) * 100 : 0,
];
}
protected function getDailyTrend(): array
{
return Trace::selectRaw('DATE(created_at) as date')
->selectRaw('SUM(estimated_cost) as cost')
->selectRaw('COUNT(*) as calls')
->where('created_at', '>=', now()->subDays(30))
->groupBy('date')
->orderBy('date')
->get()
->toArray();
}
protected function getModelBreakdown(): array
{
return DB::table('mindwave_spans')
->join('mindwave_traces', 'mindwave_spans.trace_id', '=', 'mindwave_traces.trace_id')
->select('request_model', 'provider_name')
->selectRaw('COUNT(*) as call_count')
->selectRaw('SUM(mindwave_traces.estimated_cost) as total_cost')
->selectRaw('AVG(mindwave_traces.estimated_cost) as avg_cost')
->whereNotNull('request_model')
->where('mindwave_traces.created_at', '>=', now()->subDays(30))
->groupBy('request_model', 'provider_name')
->orderByDesc('total_cost')
->get()
->toArray();
}
protected function getProviderComparison(): array
{
return DB::table('mindwave_spans')
->join('mindwave_traces', 'mindwave_spans.trace_id', '=', 'mindwave_traces.trace_id')
->select('provider_name')
->selectRaw('COUNT(*) as call_count')
->selectRaw('SUM(mindwave_traces.estimated_cost) as total_cost')
->selectRaw('AVG(mindwave_traces.estimated_cost) as avg_cost')
->selectRaw('SUM(input_tokens + output_tokens) as total_tokens')
->whereNotNull('provider_name')
->where('mindwave_traces.created_at', '>=', now()->subDays(30))
->groupBy('provider_name')
->get()
->toArray();
}
protected function getTopExpensiveTraces(): array
{
return Trace::orderByDesc('estimated_cost')
->where('created_at', '>=', now()->subDays(7))
->limit(10)
->get()
->map(fn($trace) => [
'trace_id' => $trace->trace_id,
'cost' => $trace->estimated_cost,
'duration_ms' => $trace->getDurationInMilliseconds(),
'total_tokens' => $trace->getTotalTokens(),
'created_at' => $trace->created_at,
])
->toArray();
}
protected function getProjections(): array
{
$dailyAvg = Trace::where('created_at', '>=', now()->subDays(7))
->selectRaw('AVG(daily_cost) as avg')
->fromSub(function ($query) {
$query->selectRaw('DATE(created_at) as date, SUM(estimated_cost) as daily_cost')
->from('mindwave_traces')
->groupBy('date');
}, 'daily_costs')
->value('avg');
$daysLeftInMonth = now()->endOfMonth()->diffInDays(now());
return [
'daily_average' => $dailyAvg,
'projected_month_end' => ($dailyAvg * $daysLeftInMonth) +
Trace::whereMonth('created_at', now()->month)->sum('estimated_cost'),
];
}
}Best Practices
When to Track Costs
Always track in production:
- Budget management is critical
- Cost trends inform optimization
- Real usage patterns drive decisions
Optional in development:
- Useful for testing cost impact
- But can use free models (Ollama)
Granularity Considerations
Too granular:
// Don't track every token separately
foreach ($tokens as $token) {
recordCost($token); // Too much overhead
}Just right:
// Track at request/operation level
$response = Mindwave::llm()->generateText($prompt);
// Cost automatically tracked per callToo coarse:
// Monthly invoice doesn't help daily optimization
// Use Mindwave's per-call tracking insteadStorage and Retention
Configure data retention based on needs:
// config/mindwave-tracing.php
'retention_days' => env('MINDWAVE_TRACE_RETENTION_DAYS', 30),Recommendations:
- 7 days: Minimum for debugging and optimization
- 30 days: Good balance for monthly analysis
- 90 days: Quarterly trends and year-over-year comparison
- 365+ days: Long-term analytics (consider archiving)
Prune old data regularly:
# Schedule in app/Console/Kernel.php
$schedule->command('mindwave:prune-traces --days=30')->daily();Privacy Considerations
Cost data is NOT sensitive:
- Token counts: Safe to store
- Model names: Safe to store
- Estimated costs: Safe to store
- Timestamps: Safe to store
But prompts/responses ARE sensitive:
- Disable by default:
capture_messages => false - Only enable in development
- Never log user input/output in production
Aggregate reporting is safe:
- "Total spend today: $45.67" ✓
- "GPT-4 cost $23.00 across 100 calls" ✓
- "User X asked about..." ✗ (too specific)
Troubleshooting
Inaccurate Cost Calculations
Problem: Costs don't match provider invoices
Causes:
- Outdated pricing in config
- Special pricing agreements
- Token counting differences
- Model version mismatches
Solutions:
// 1. Update pricing regularly
// Check OpenAI pricing: https://openai.com/pricing
// Check Anthropic pricing: https://anthropic.com/pricing
'pricing' => [
'openai' => [
'gpt-4' => [
'input' => 0.03, // Update these
'output' => 0.06, // from provider
],
],
],
// 2. Add special pricing for your account
'pricing' => [
'openai' => [
'gpt-4-custom' => [
'input' => 0.025, // Your negotiated rate
'output' => 0.05,
],
],
],
// 3. Compare actual invoice to tracked costs
$trackedCost = Trace::whereMonth('created_at', 10)
->whereYear('created_at', 2024)
->sum('estimated_cost');
$actualInvoice = 1234.56; // From OpenAI invoice
$difference = $actualInvoice - $trackedCost;
$percentOff = ($difference / $actualInvoice) * 100;
echo "Tracked: $" . number_format($trackedCost, 2) . "\n";
echo "Invoice: $" . number_format($actualInvoice, 2) . "\n";
echo "Difference: " . round($percentOff, 1) . "%\n";
// Adjust pricing if consistently offMissing Pricing Data
Problem: Some models show $0.00 cost
Cause: Model not in pricing config
Solution:
// Add missing model pricing
'pricing' => [
'openai' => [
// Add new models as they're released
'gpt-4o' => [
'input' => 0.005,
'output' => 0.015,
],
],
],
// Or check logs for unpriced models
php artisan mindwave:trace-stats --provider=openaiCost Drift Over Time
Problem: Costs gradually differ from invoices
Cause: Provider pricing changes
Best Practice:
// Set reminder to review pricing quarterly
// config/mindwave-tracing.php
'cost_estimation' => [
'enabled' => true,
// Document last update date
'pricing_last_updated' => '2025-01-15',
'pricing' => [
// ... pricing data
],
],Create monitoring:
// Check for significant cost discrepancies
$monthlyTracked = Trace::whereMonth('created_at', now()->month)
->sum('estimated_cost');
if ($monthlyTracked > 1000) { // Only for significant volumes
\Log::info('Monthly LLM cost checkpoint', [
'tracked_cost' => $monthlyTracked,
'note' => 'Compare to invoice at month end',
]);
}Summary
Mindwave's cost tracking provides:
- ✅ Automatic calculation on every LLM call
- ✅ Real-time visibility through events
- ✅ Database storage for flexible querying
- ✅ Multi-dimensional attribution (user, feature, model)
- ✅ Budget management with alerts
- ✅ Optimization insights from actual data
- ✅ Production-ready architecture
Key takeaways:
- Costs are tracked automatically - no extra code needed
- Use events for real-time monitoring and alerts
- Query database for historical analysis and dashboards
- Implement per-user/per-tenant budgets for SaaS
- Update pricing config quarterly as providers change rates
- Use cost data to inform model selection and prompt optimization
Next steps:
- Distributed Tracing - Track costs across services
- Production - Best practices for production cost tracking