Skip to content

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:

php
{
  "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_tokens
  • gen_ai.usage.output_tokens
  • gen_ai.usage.total_tokens
  • gen_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 Cost

Example calculation:

php
// 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.093

Integration with Tracing

Costs are automatically attached to spans in the estimated_cost field. This means:

  1. Every LLM call has its cost calculated automatically
  2. Stored in database for querying and analysis
  3. Aggregated at trace level for total request cost
  4. Available in real-time through events and logs
  5. 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:

php
// 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:

dotenv
# Enable automatic cost calculation (default: true)
MINDWAVE_COST_ESTIMATION_ENABLED=true

Custom Pricing for Self-Hosted Models

If you're using self-hosted or custom models, add their pricing:

php
// 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:

php
// 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:

php
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:

php
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:

php
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:

php
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:

php
// 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:

php
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:

php
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:

php
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:

php
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:

php
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:

php
$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:

bash
# 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-4

Example 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 programmatically

Honeycomb 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:

txt
# 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:

php
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 cost

2. Prompt Engineering for Efficiency

Track how prompt changes affect costs:

php
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 prompt

3. Using PromptComposer to Reduce Token Usage

Mindwave's PromptComposer helps minimize input tokens:

php
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:

php
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:

php
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
<?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:

php
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
<?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
<?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
<?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:

php
// Don't track every token separately
foreach ($tokens as $token) {
    recordCost($token);  // Too much overhead
}

Just right:

php
// Track at request/operation level
$response = Mindwave::llm()->generateText($prompt);
// Cost automatically tracked per call

Too coarse:

php
// Monthly invoice doesn't help daily optimization
// Use Mindwave's per-call tracking instead

Storage and Retention

Configure data retention based on needs:

php
// 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:

bash
# 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:

  1. Outdated pricing in config
  2. Special pricing agreements
  3. Token counting differences
  4. Model version mismatches

Solutions:

php
// 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 off

Missing Pricing Data

Problem: Some models show $0.00 cost

Cause: Model not in pricing config

Solution:

php
// 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=openai

Cost Drift Over Time

Problem: Costs gradually differ from invoices

Cause: Provider pricing changes

Best Practice:

php
// 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:

php
// 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:

  1. Costs are tracked automatically - no extra code needed
  2. Use events for real-time monitoring and alerts
  3. Query database for historical analysis and dashboards
  4. Implement per-user/per-tenant budgets for SaaS
  5. Update pricing config quarterly as providers change rates
  6. Use cost data to inform model selection and prompt optimization

Next steps: