Events
Mindwave fires Laravel events throughout the LLM lifecycle, providing powerful hooks for custom behavior, monitoring, analytics, and integrations. Events enable you to react to LLM operations without modifying core Mindwave code.
Overview
Mindwave leverages Laravel's native event system to dispatch events at critical points during LLM interactions. This allows you to:
- Track Usage: Log all LLM calls to your database or analytics platform
- Monitor Performance: Alert on slow requests or high costs
- Implement Business Logic: Deduct user credits, update quotas, enforce rate limits
- Debug & Troubleshoot: Capture detailed information about failures
- Build Real-Time UIs: Stream token deltas to frontend applications
- Integrate External Tools: Send data to Slack, monitoring systems, or data warehouses
All events integrate seamlessly with Laravel's event system, supporting synchronous listeners, queued listeners for async processing, and Event::fake() for testing.
Available Events
Mindwave fires four core events during the LLM lifecycle:
LlmRequestStarted
Fired when an LLM request begins, before calling the provider API.
Event Class: Mindwave\Mindwave\Observability\Events\LlmRequestStarted
Properties:
public readonly string $provider; // 'openai', 'anthropic', 'ollama', etc.
public readonly string $model; // 'gpt-4', 'claude-3-opus', etc.
public readonly string $operation; // 'chat', 'text_completion', 'embeddings'
public readonly ?array $messages; // Input messages (null if capture disabled)
public readonly array $parameters; // Request parameters (temperature, max_tokens, etc.)
public readonly string $spanId; // OpenTelemetry span ID
public readonly string $traceId; // OpenTelemetry trace ID
public readonly int $timestamp; // Request start time (nanoseconds)Helper Methods:
$event->getParameter('temperature'); // Get specific parameter
$event->hasMessages(); // Check if messages captured
$event->getTimestampInSeconds(); // Get timestamp in seconds
$event->getTimestampInMilliseconds(); // Get timestamp in milliseconds
$event->toArray(); // Convert to arrayUse Cases:
- Log request start times
- Increment request counters
- Start performance timers
- Validate request parameters
- Implement pre-request hooks
LlmResponseCompleted
Fired when an LLM request completes successfully.
Event Class: Mindwave\Mindwave\Observability\Events\LlmResponseCompleted
Properties:
public readonly string $provider; // Provider name
public readonly string $model; // Model used
public readonly string $operation; // Operation type
public readonly array $response; // Full response data
public readonly array $tokenUsage; // Token usage breakdown
public readonly int $duration; // Request duration (nanoseconds)
public readonly ?float $costEstimate; // Estimated cost in USD
public readonly string $spanId; // OpenTelemetry span ID
public readonly string $traceId; // OpenTelemetry trace ID
public readonly int $timestamp; // Completion timestamp (nanoseconds)
public readonly array $metadata; // Additional metadataHelper Methods:
// Response Information
$event->getResponseId(); // Get response ID
$event->getFinishReason(); // Get finish reason
$event->getFinishReasons(); // Get all finish reasons (multiple choices)
// Token Usage
$event->getInputTokens(); // Input token count
$event->getOutputTokens(); // Output token count
$event->getTotalTokens(); // Total tokens
$event->getCacheReadTokens(); // Cache read tokens
$event->getCacheCreationTokens(); // Cache creation tokens
$event->usedCache(); // Check if cache was used
// Performance Metrics
$event->getDurationInSeconds(); // Duration in seconds
$event->getDurationInMilliseconds(); // Duration in milliseconds
$event->getTokensPerSecond(); // Tokens generated per second
// Cost Information
$event->hasCostEstimate(); // Check if cost available
$event->getFormattedCost(4); // Get formatted cost (e.g., "$0.0042")
// Metadata & Timestamps
$event->getMetadata('key', 'default'); // Get metadata value
$event->getTimestampInSeconds(); // Timestamp in seconds
$event->toArray(); // Convert to arrayUse Cases:
- Log successful completions
- Track token usage and costs
- Calculate user charges
- Monitor performance metrics
- Update analytics dashboards
- Trigger downstream workflows
LlmErrorOccurred
Fired when an LLM request fails with an error.
Event Class: Mindwave\Mindwave\Observability\Events\LlmErrorOccurred
Properties:
public readonly Throwable $exception; // The exception that occurred
public readonly string $provider; // Provider name
public readonly string $model; // Model being used
public readonly string $operation; // Operation type
public readonly string $spanId; // OpenTelemetry span ID
public readonly string $traceId; // OpenTelemetry trace ID
public readonly int $timestamp; // Error timestamp (nanoseconds)
public readonly array $context; // Additional error contextHelper Methods:
// Exception Information
$event->getMessage(); // Exception message
$event->getCode(); // Exception code
$event->getExceptionClass(); // Exception class name
$event->getFile(); // File where exception occurred
$event->getLine(); // Line number
$event->getTrace(); // Stack trace as string
// Previous Exceptions
$event->getPrevious(); // Get previous exception
$event->hasPrevious(); // Check if has previous exception
// Context & Error Info
$event->getContext('key', 'default'); // Get context value
$event->getErrorInfo(); // Get error info array
$event->getTimestampInSeconds(); // Timestamp in seconds
$event->toArray(); // Convert to arrayUse Cases:
- Alert on critical failures
- Log errors to monitoring systems
- Send Slack notifications
- Implement retry logic
- Track error rates
- Debug production issues
LlmTokenStreamed
Fired for each token delta during streaming responses.
Event Class: Mindwave\Mindwave\Observability\Events\LlmTokenStreamed
Properties:
public readonly string $delta; // New content chunk
public readonly int $cumulativeTokens; // Total tokens streamed so far
public readonly string $spanId; // OpenTelemetry span ID
public readonly string $traceId; // OpenTelemetry trace ID
public readonly int $timestamp; // Token received timestamp (nanoseconds)
public readonly array $metadata; // Additional metadataHelper Methods:
$event->getDeltaLength(); // Length of delta in characters
$event->getMetadata('key', 'default'); // Get metadata value
$event->isFinal(); // Check if this is the final token
$event->getFinishReason(); // Get finish reason (if final)
$event->getTimestampInSeconds(); // Timestamp in seconds
$event->toArray(); // Convert to arrayUse Cases:
- Build real-time streaming UIs
- Track streaming progress
- Calculate tokens per second
- Update progress bars
- Send Server-Sent Events (SSE)
- Debug streaming issues
Listening to Events
Creating Event Listeners
Create a listener class for any Mindwave event:
<?php
namespace App\Listeners;
use Mindwave\Mindwave\Observability\Events\LlmResponseCompleted;
use Illuminate\Support\Facades\Log;
class LogLlmCompletion
{
public function handle(LlmResponseCompleted $event): void
{
Log::info('LLM request completed', [
'model' => $event->model,
'tokens' => $event->getTotalTokens(),
'duration' => $event->getDurationInMilliseconds() . 'ms',
'cost' => $event->getFormattedCost(),
]);
}
}Registering Listeners
Register your listener in app/Providers/EventServiceProvider.php:
use Mindwave\Mindwave\Observability\Events\LlmResponseCompleted;
use App\Listeners\LogLlmCompletion;
protected $listen = [
LlmResponseCompleted::class => [
LogLlmCompletion::class,
],
];Closure-Based Listeners
For simple use cases, register closure listeners in a service provider:
use Illuminate\Support\Facades\Event;
use Mindwave\Mindwave\Observability\Events\LlmRequestStarted;
Event::listen(LlmRequestStarted::class, function (LlmRequestStarted $event) {
logger()->debug("LLM request started: {$event->model}");
});Queued Listeners
Process events asynchronously for better performance:
<?php
namespace App\Listeners;
use Illuminate\Contracts\Queue\ShouldQueue;
use Mindwave\Mindwave\Observability\Events\LlmResponseCompleted;
class StoreAnalytics implements ShouldQueue
{
public $queue = 'analytics';
public function handle(LlmResponseCompleted $event): void
{
// Heavy processing happens async on queue
Analytics::track('llm_completion', [
'model' => $event->model,
'tokens' => $event->getTotalTokens(),
'cost' => $event->costEstimate,
]);
}
}Event Subscribers
Group multiple event handlers in a single class:
<?php
namespace App\Listeners;
use Illuminate\Events\Dispatcher;
use Mindwave\Mindwave\Observability\Events\LlmRequestStarted;
use Mindwave\Mindwave\Observability\Events\LlmResponseCompleted;
use Mindwave\Mindwave\Observability\Events\LlmErrorOccurred;
class LlmEventSubscriber
{
public function handleRequestStarted(LlmRequestStarted $event): void
{
// Handle request start
}
public function handleResponseCompleted(LlmResponseCompleted $event): void
{
// Handle completion
}
public function handleError(LlmErrorOccurred $event): void
{
// Handle errors
}
public function subscribe(Dispatcher $events): array
{
return [
LlmRequestStarted::class => 'handleRequestStarted',
LlmResponseCompleted::class => 'handleResponseCompleted',
LlmErrorOccurred::class => 'handleError',
];
}
}Register the subscriber:
// In EventServiceProvider
protected $subscribe = [
\App\Listeners\LlmEventSubscriber::class,
];Common Use Cases
Database Logging
Store all LLM calls in your database for analytics and auditing:
Migration:
Schema::create('llm_logs', function (Blueprint $table) {
$table->id();
$table->string('trace_id')->index();
$table->string('provider');
$table->string('model');
$table->string('operation');
$table->integer('input_tokens');
$table->integer('output_tokens');
$table->integer('total_tokens');
$table->float('duration_ms');
$table->decimal('cost', 10, 6)->nullable();
$table->string('status'); // 'success' or 'error'
$table->text('error_message')->nullable();
$table->timestamps();
});Listener:
<?php
namespace App\Listeners;
use App\Models\LlmLog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Mindwave\Mindwave\Observability\Events\LlmResponseCompleted;
use Mindwave\Mindwave\Observability\Events\LlmErrorOccurred;
class StoreLlmLog implements ShouldQueue
{
public function handleSuccess(LlmResponseCompleted $event): void
{
LlmLog::create([
'trace_id' => $event->traceId,
'provider' => $event->provider,
'model' => $event->model,
'operation' => $event->operation,
'input_tokens' => $event->getInputTokens(),
'output_tokens' => $event->getOutputTokens(),
'total_tokens' => $event->getTotalTokens(),
'duration_ms' => $event->getDurationInMilliseconds(),
'cost' => $event->costEstimate,
'status' => 'success',
]);
}
public function handleError(LlmErrorOccurred $event): void
{
LlmLog::create([
'trace_id' => $event->traceId,
'provider' => $event->provider,
'model' => $event->model,
'operation' => $event->operation,
'status' => 'error',
'error_message' => $event->getMessage(),
]);
}
public function subscribe($events): array
{
return [
LlmResponseCompleted::class => 'handleSuccess',
LlmErrorOccurred::class => 'handleError',
];
}
}Slack Notifications
Send alerts to Slack when LLM errors occur or costs are high:
<?php
namespace App\Listeners;
use Illuminate\Support\Facades\Http;
use Mindwave\Mindwave\Observability\Events\LlmErrorOccurred;
use Mindwave\Mindwave\Observability\Events\LlmResponseCompleted;
class SlackNotifier
{
private string $webhookUrl;
public function __construct()
{
$this->webhookUrl = config('services.slack.webhook_url');
}
public function handleError(LlmErrorOccurred $event): void
{
Http::post($this->webhookUrl, [
'text' => ':rotating_light: LLM Error Occurred',
'blocks' => [
[
'type' => 'section',
'text' => [
'type' => 'mrkdwn',
'text' => "*LLM Request Failed*\n" .
"*Model:* {$event->model}\n" .
"*Provider:* {$event->provider}\n" .
"*Error:* {$event->getMessage()}\n" .
"*Trace ID:* `{$event->traceId}`",
],
],
],
]);
}
public function handleHighCost(LlmResponseCompleted $event): void
{
// Alert if single request costs more than $0.10
if ($event->costEstimate && $event->costEstimate > 0.10) {
Http::post($this->webhookUrl, [
'text' => ':money_with_wings: High-cost LLM request detected',
'blocks' => [
[
'type' => 'section',
'text' => [
'type' => 'mrkdwn',
'text' => "*High-Cost Request*\n" .
"*Model:* {$event->model}\n" .
"*Cost:* {$event->getFormattedCost()}\n" .
"*Tokens:* {$event->getTotalTokens()}\n" .
"*Trace ID:* `{$event->traceId}`",
],
],
],
]);
}
}
public function subscribe($events): array
{
return [
LlmErrorOccurred::class => 'handleError',
LlmResponseCompleted::class => 'handleHighCost',
];
}
}User Credits System
Deduct credits from user accounts after successful LLM calls:
<?php
namespace App\Listeners;
use App\Models\User;
use Illuminate\Contracts\Queue\ShouldQueue;
use Mindwave\Mindwave\Observability\Events\LlmResponseCompleted;
class DeductUserCredits implements ShouldQueue
{
public function handle(LlmResponseCompleted $event): void
{
// Get user from trace context (you'd add this to metadata)
$userId = $event->getMetadata('user_id');
if (!$userId) {
return;
}
$user = User::find($userId);
if (!$user) {
return;
}
// Deduct credits based on token usage
// Example: 1 credit per 1000 tokens
$creditsToDeduct = ceil($event->getTotalTokens() / 1000);
$user->decrement('credits', $creditsToDeduct);
// Log the transaction
$user->creditTransactions()->create([
'amount' => -$creditsToDeduct,
'description' => "LLM usage: {$event->model}",
'trace_id' => $event->traceId,
'tokens' => $event->getTotalTokens(),
]);
}
}Custom Metrics
Send metrics to Prometheus, StatsD, or other monitoring systems:
<?php
namespace App\Listeners;
use Prometheus\CollectorRegistry;
use Mindwave\Mindwave\Observability\Events\LlmResponseCompleted;
class RecordPrometheusMetrics
{
private CollectorRegistry $registry;
public function __construct(CollectorRegistry $registry)
{
$this->registry = $registry;
}
public function handle(LlmResponseCompleted $event): void
{
// Counter: Total LLM requests
$counter = $this->registry->getOrRegisterCounter(
'llm',
'requests_total',
'Total LLM requests',
['provider', 'model', 'operation']
);
$counter->inc([
$event->provider,
$event->model,
$event->operation
]);
// Histogram: Request duration
$histogram = $this->registry->getOrRegisterHistogram(
'llm',
'request_duration_seconds',
'LLM request duration',
['provider', 'model']
);
$histogram->observe(
$event->getDurationInSeconds(),
[$event->provider, $event->model]
);
// Histogram: Token usage
$tokenHistogram = $this->registry->getOrRegisterHistogram(
'llm',
'tokens_total',
'Total tokens used',
['provider', 'model', 'type']
);
$tokenHistogram->observe(
$event->getInputTokens(),
[$event->provider, $event->model, 'input']
);
$tokenHistogram->observe(
$event->getOutputTokens(),
[$event->provider, $event->model, 'output']
);
// Gauge: Estimated cost
if ($event->hasCostEstimate()) {
$gauge = $this->registry->getOrRegisterGauge(
'llm',
'cost_estimate_usd',
'Estimated cost in USD',
['provider', 'model']
);
$gauge->set(
$event->costEstimate,
[$event->provider, $event->model]
);
}
}
}Rate Limiting
Implement custom rate limiting based on LLM usage:
<?php
namespace App\Listeners;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Cache;
use Mindwave\Mindwave\Observability\Events\LlmRequestStarted;
use Mindwave\Mindwave\Observability\Events\LlmResponseCompleted;
class EnforceLlmRateLimits
{
public function handleStart(LlmRequestStarted $event): void
{
$userId = $event->parameters['user_id'] ?? null;
if (!$userId) {
return;
}
// Allow 100 requests per hour
$key = "llm-requests:{$userId}";
if (RateLimiter::tooManyAttempts($key, 100)) {
throw new \Exception('Too many LLM requests. Please try again later.');
}
RateLimiter::hit($key, 3600); // 1 hour
}
public function handleCompletion(LlmResponseCompleted $event): void
{
$userId = $event->getMetadata('user_id');
if (!$userId) {
return;
}
// Track token usage per day
$date = now()->format('Y-m-d');
$key = "llm-tokens:{$userId}:{$date}";
$currentTokens = Cache::get($key, 0);
$newTokens = $currentTokens + $event->getTotalTokens();
Cache::put($key, $newTokens, now()->endOfDay());
// Alert if user exceeds daily quota (e.g., 100k tokens)
if ($newTokens > 100_000) {
// Send notification or throttle future requests
event(new \App\Events\UserExceededTokenQuota($userId, $newTokens));
}
}
public function subscribe($events): array
{
return [
LlmRequestStarted::class => 'handleStart',
LlmResponseCompleted::class => 'handleCompletion',
];
}
}Real-Time Streaming UI
Build a real-time streaming interface using Server-Sent Events:
Controller:
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Event;
use Mindwave\Mindwave\Facades\Mindwave;
use Mindwave\Mindwave\Observability\Events\LlmTokenStreamed;
use Symfony\Component\HttpFoundation\StreamedResponse;
class ChatController extends Controller
{
public function stream(Request $request)
{
$prompt = $request->input('prompt');
return response()->stream(function () use ($prompt) {
// Set up SSE headers
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
// Listen for token events
$tokens = [];
Event::listen(LlmTokenStreamed::class, function ($event) use (&$tokens) {
$tokens[] = $event->delta;
// Send SSE event to frontend
echo "data: " . json_encode([
'delta' => $event->delta,
'cumulative_tokens' => $event->cumulativeTokens,
'is_final' => $event->isFinal(),
]) . "\n\n";
ob_flush();
flush();
});
// Generate streaming response
$stream = Mindwave::streamText($prompt);
// Consume the stream (events fire automatically)
foreach ($stream as $delta) {
// Tokens are sent via event listener above
}
// Send final event
echo "data: " . json_encode(['done' => true]) . "\n\n";
ob_flush();
flush();
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no',
]);
}
}Frontend (JavaScript):
const eventSource = new EventSource(
'/api/chat/stream?prompt=' + encodeURIComponent(prompt)
);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.done) {
eventSource.close();
return;
}
// Append delta to UI
appendToChat(data.delta);
// Update token counter
updateTokenCount(data.cumulative_tokens);
};
eventSource.onerror = (error) => {
console.error('Stream error:', error);
eventSource.close();
};Analytics Integration
Send events to analytics platforms like Mixpanel or Segment:
<?php
namespace App\Listeners;
use Illuminate\Contracts\Queue\ShouldQueue;
use Mindwave\Mindwave\Observability\Events\LlmResponseCompleted;
use Segment;
class TrackLlmAnalytics implements ShouldQueue
{
public function handle(LlmResponseCompleted $event): void
{
$userId = $event->getMetadata('user_id');
if (!$userId) {
return;
}
Segment::track([
'userId' => $userId,
'event' => 'LLM Completion',
'properties' => [
'provider' => $event->provider,
'model' => $event->model,
'operation' => $event->operation,
'input_tokens' => $event->getInputTokens(),
'output_tokens' => $event->getOutputTokens(),
'total_tokens' => $event->getTotalTokens(),
'duration_ms' => $event->getDurationInMilliseconds(),
'cost_usd' => $event->costEstimate,
'used_cache' => $event->usedCache(),
'tokens_per_second' => round($event->getTokensPerSecond(), 2),
],
]);
}
}Testing with Events
Mindwave events integrate seamlessly with Laravel's event testing utilities:
Basic Event Testing
use Illuminate\Support\Facades\Event;
use Mindwave\Mindwave\Facades\Mindwave;
use Mindwave\Mindwave\Observability\Events\LlmResponseCompleted;
test('fires LlmResponseCompleted event on success', function () {
Event::fake([LlmResponseCompleted::class]);
Mindwave::generateText('Hello, world!');
Event::assertDispatched(LlmResponseCompleted::class);
});Testing Event Data
test('event contains correct token usage data', function () {
Event::fake();
Mindwave::generateText('Test prompt');
Event::assertDispatched(LlmResponseCompleted::class, function ($event) {
return $event->model === 'gpt-4' &&
$event->getTotalTokens() > 0 &&
$event->hasCostEstimate();
});
});Testing Event Listeners
use App\Listeners\LogLlmCompletion;
test('LogLlmCompletion listener logs correctly', function () {
Log::spy();
$event = new LlmResponseCompleted(
provider: 'openai',
model: 'gpt-4',
operation: 'chat',
response: ['id' => 'test'],
tokenUsage: ['input_tokens' => 10, 'output_tokens' => 20],
duration: 1000000000,
costEstimate: 0.0042,
spanId: 'span-123',
traceId: 'trace-456',
timestamp: hrtime(true)
);
$listener = new LogLlmCompletion();
$listener->handle($event);
Log::shouldHaveReceived('info')
->once()
->with('LLM request completed', Mockery::any());
});Testing Streaming Events
test('fires LlmTokenStreamed events during streaming', function () {
Event::fake([LlmTokenStreamed::class]);
$stream = Mindwave::streamText('Test prompt');
// Consume the stream
foreach ($stream as $delta) {
// Events fire as stream is consumed
}
Event::assertDispatched(LlmTokenStreamed::class);
// Verify multiple events were fired
Event::assertDispatchedTimes(LlmTokenStreamed::class, function ($times) {
return $times > 1;
});
});Testing Error Events
use Mindwave\Mindwave\Observability\Events\LlmErrorOccurred;
test('fires LlmErrorOccurred on API failure', function () {
Event::fake([LlmErrorOccurred::class]);
// Mock an API failure
Http::fake([
'api.openai.com/*' => Http::response([], 500),
]);
expect(fn () => Mindwave::generateText('Test'))
->toThrow(Exception::class);
Event::assertDispatched(LlmErrorOccurred::class, function ($event) {
return str_contains($event->getMessage(), 'API error');
});
});Streaming Events
Streaming responses fire events differently than standard requests:
Event Flow for Streaming
- LlmRequestStarted - Fired once when stream begins
- LlmTokenStreamed - Fired for each token/chunk received
- LlmResponseCompleted - Fired once when stream completes (or LlmErrorOccurred on failure)
Tracking Progress
use Mindwave\Mindwave\Observability\Events\LlmTokenStreamed;
use Illuminate\Support\Facades\Event;
Event::listen(LlmTokenStreamed::class, function ($event) {
// Track streaming progress
cache()->put(
"stream-progress:{$event->traceId}",
[
'tokens' => $event->cumulativeTokens,
'latest_delta' => $event->delta,
'is_complete' => $event->isFinal(),
],
now()->addMinutes(5)
);
});Calculating Tokens Per Second
Event::listen(LlmTokenStreamed::class, function ($event) {
$startTime = cache()->get("stream-start:{$event->traceId}");
if (!$startTime) {
// First token - store start time
cache()->put("stream-start:{$event->traceId}", $event->timestamp);
return;
}
$elapsed = ($event->timestamp - $startTime) / 1_000_000_000; // nanoseconds to seconds
$tokensPerSecond = $event->cumulativeTokens / $elapsed;
logger()->debug("Streaming performance: {$tokensPerSecond} tokens/sec");
});Building Progress Bars
use Mindwave\Mindwave\Observability\Events\LlmTokenStreamed;
$maxTokens = 1000; // Expected max tokens
$progressBar = null;
Event::listen(LlmTokenStreamed::class, function ($event) use (&$progressBar, $maxTokens) {
if (!$progressBar) {
$progressBar = new ProgressBar($maxTokens);
}
$progressBar->setProgress($event->cumulativeTokens);
if ($event->isFinal()) {
$progressBar->finish();
}
});Performance Considerations
Use Queued Listeners for Heavy Processing
Always use ShouldQueue for listeners that perform heavy operations:
use Illuminate\Contracts\Queue\ShouldQueue;
class HeavyAnalyticsProcessor implements ShouldQueue
{
public $queue = 'analytics'; // Use dedicated queue
public $connection = 'redis';
public $tries = 3;
public $timeout = 120;
public function handle(LlmResponseCompleted $event): void
{
// Heavy database queries, API calls, etc.
}
}Batch Processing
For high-volume applications, batch process events:
use Illuminate\Contracts\Queue\ShouldQueue;
class BatchLlmLogger implements ShouldQueue
{
private const BATCH_SIZE = 100;
public function handle(LlmResponseCompleted $event): void
{
// Add to batch
$batch = cache()->get('llm-log-batch', []);
$batch[] = $event->toArray();
// Insert when batch is full
if (count($batch) >= self::BATCH_SIZE) {
DB::table('llm_logs')->insert($batch);
cache()->forget('llm-log-batch');
} else {
cache()->put('llm-log-batch', $batch, now()->addMinutes(5));
}
}
}Selective Event Listening
Only listen to events you need:
// ❌ Bad: Listen to all events even if not needed
Event::listen(LlmTokenStreamed::class, function ($event) {
// Do nothing for most tokens
if ($event->isFinal()) {
// Only care about final token
}
});
// ✅ Good: Filter in listener or use conditions
Event::listen(LlmTokenStreamed::class, function ($event) {
// Early return for non-final tokens
if (!$event->isFinal()) {
return;
}
// Process final token
});Disable Events When Not Needed
For testing or batch operations where you don't need events:
use Illuminate\Support\Facades\Event;
Event::fake(); // Disable all events
// or selectively fake specific events
Event::fake([
LlmResponseCompleted::class,
]);
// Events won't be dispatched
Mindwave::generateText('Test');Memory Management for Streaming
Be cautious with memory when listening to streaming events:
// ❌ Bad: Storing all deltas in memory
$allDeltas = [];
Event::listen(LlmTokenStreamed::class, function ($event) use (&$allDeltas) {
$allDeltas[] = $event->delta; // Memory grows with each token
});
// ✅ Good: Process deltas immediately
Event::listen(LlmTokenStreamed::class, function ($event) {
// Send to frontend immediately
broadcast(new TokenReceived($event->delta));
// Or write to stream
fwrite($stream, $event->delta);
});Best Practices
1. Keep Listeners Focused
Each listener should have a single responsibility:
// ❌ Bad: One listener doing too much
class LlmEventHandler
{
public function handle(LlmResponseCompleted $event)
{
$this->logToDatabase($event);
$this->sendToAnalytics($event);
$this->updateUserCredits($event);
$this->sendSlackNotification($event);
// Too many responsibilities!
}
}
// ✅ Good: Separate listeners for each concern
class LogLlmToDatabase { /* ... */ }
class SendLlmToAnalytics implements ShouldQueue { /* ... */ }
class UpdateUserCredits implements ShouldQueue { /* ... */ }
class NotifySlackOnHighCost implements ShouldQueue { /* ... */ }2. Handle Errors Gracefully
Never let listener errors crash your application:
class SafeLlmLogger
{
public function handle(LlmResponseCompleted $event): void
{
try {
// Logging logic
DB::table('llm_logs')->insert($event->toArray());
} catch (\Throwable $e) {
// Log the error but don't crash
report($e);
// Optionally retry with exponential backoff
if ($this->attempts() < 3) {
$this->release(60 * $this->attempts());
}
}
}
}3. Use Type Hints
Always type hint event parameters for better IDE support:
use Mindwave\Mindwave\Observability\Events\LlmResponseCompleted;
Event::listen(LlmResponseCompleted::class, function (LlmResponseCompleted $event) {
// Full IDE autocomplete for $event->
$tokens = $event->getTotalTokens();
});4. Document Event Dependencies
Make event dependencies explicit in listener constructors:
class StoreLlmAnalytics implements ShouldQueue
{
private AnalyticsService $analytics;
public function __construct(AnalyticsService $analytics)
{
$this->analytics = $analytics;
}
public function handle(LlmResponseCompleted $event): void
{
$this->analytics->track('llm_completion', $event->toArray());
}
}5. Consider Privacy
Be mindful of sensitive data in events:
public function handle(LlmRequestStarted $event): void
{
// Messages might contain PII
if ($event->hasMessages()) {
// Only log if explicitly configured
if (config('app.log_llm_messages')) {
Log::debug('Messages', $event->messages);
}
}
// Safe to log metadata
Log::info('Request started', [
'model' => $event->model,
'trace_id' => $event->traceId,
]);
}6. Test Event Listeners
Write tests for all custom listeners:
test('StoreLlmLog creates database record', function () {
$event = new LlmResponseCompleted(/* ... */);
$listener = new StoreLlmLog();
$listener->handle($event);
expect(DB::table('llm_logs')->count())->toBe(1);
$log = DB::table('llm_logs')->first();
expect($log->model)->toBe($event->model);
expect($log->total_tokens)->toBe($event->getTotalTokens());
});7. Use Events for Cross-Cutting Concerns
Events are perfect for cross-cutting concerns that shouldn't be in core business logic:
- Logging and monitoring
- Analytics and metrics
- Notifications and alerts
- Quota and rate limiting
- Audit trails
- External integrations
8. Avoid Circular Dependencies
Be careful not to trigger LLM calls inside LLM event listeners:
// ❌ Dangerous: Could cause infinite loop
Event::listen(LlmResponseCompleted::class, function ($event) {
// This triggers another LLM call, which fires another event...
Mindwave::generateText('Analyze this: ' . $event->response);
});
// ✅ Safe: Process data without triggering new LLM calls
Event::listen(LlmResponseCompleted::class, function ($event) {
// Safe operations: database, logging, HTTP calls
DB::table('analytics')->insert($event->toArray());
});Event Configuration
The built-in TraceEventSubscriber respects configuration options:
// config/mindwave-llm.php
return [
'tracing' => [
// Enable/disable event logging
'log_events' => env('MINDWAVE_LOG_EVENTS', false),
// Slow request threshold (milliseconds)
'slow_threshold_ms' => env('MINDWAVE_SLOW_THRESHOLD_MS', 5000),
// High cost threshold (USD)
'cost_threshold' => env('MINDWAVE_COST_THRESHOLD', 0.1),
],
];When log_events is enabled, Mindwave automatically logs:
- All request starts (debug level)
- All completions with token/cost data (info level)
- Slow requests exceeding threshold (warning level)
- High-cost requests exceeding threshold (warning level)
- All errors (error level)
Summary
Mindwave's event system provides powerful hooks into the LLM lifecycle:
- Four core events: LlmRequestStarted, LlmResponseCompleted, LlmErrorOccurred, LlmTokenStreamed
- Rich event data: Tokens, costs, performance metrics, trace context
- Laravel integration: Standard event listeners, queued processing, Event::fake()
- Real-world use cases: Logging, monitoring, analytics, credits, rate limiting, streaming UIs
- Production-ready: Async processing, error handling, batching, memory management
Use events to build robust, observable LLM-powered applications without coupling your monitoring and business logic to core Mindwave functionality.
Related Documentation
- Tracing - OpenTelemetry distributed tracing
- Cost Tracking - Track and analyze LLM costs
- Testing - Testing with Event::fake()