Events System
When to Use
Use events when you need to intercept AI requests or responses across all operations without modifying providers. Use Guardrails when you need content filtering. Use events for logging, caching, authentication override, or telemetry.
All AI operations fire events via Symfony's event dispatcher. Subscribe to modify requests, log responses, or add custom behavior.
Decision
| Situation | Choose | Why |
|---|---|---|
| Modify input before call | PreGenerateResponseEvent |
Can rewrite input, change auth, force output |
| Cache/return early | PreGenerateResponseEvent + setForcedOutputObject() |
Short-circuits the provider call |
| Log or audit responses | PostGenerateResponseEvent |
Fires after non-streamed response |
| Audit streamed responses | PostStreamingResponseEvent |
Read-only; fires after stream completes |
| Handle provider exceptions | AiExceptionEvent (1.4) |
Rewrite error or inject fallback output |
Pattern
use Drupal\ai\Event\PreGenerateResponseEvent;
use Drupal\ai\Event\PostGenerateResponseEvent;
class MyEventSubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents(): array {
return [
PreGenerateResponseEvent::EVENT_NAME => ['onPreGenerate', 0],
PostGenerateResponseEvent::EVENT_NAME => ['onPostGenerate', 0],
];
}
public function onPreGenerate(PreGenerateResponseEvent $event): void {
$operationType = $event->getOperationType();
$tags = $event->getTags();
$input = $event->getInput();
// Modify input, check cache, add context
$event->setInput($modifiedInput);
}
public function onPostGenerate(PostGenerateResponseEvent $event): void {
$output = $event->getOutput();
$tokenUsage = $event->getTokenUsage(); // if available
// Log, transform, cache, or audit
}
}
Event Types
| Event | Constant | When | Can Modify |
|---|---|---|---|
PreGenerateResponseEvent |
ai.pre_generate_response |
Before provider call | Input, config, auth, tags; can force output |
PostGenerateResponseEvent |
ai.post_generate_response |
After non-streamed response | Output (post-process) |
PostStreamingResponseEvent |
ai.post_streaming_response |
After streamed response completes | Read-only (collection only) |
ProviderDisabledEvent |
-- | Provider marked unavailable | -- |
AiExceptionEvent |
-- | Changed in 1.4: Fired by ProviderProxy when a provider throws an exception | Rewrite message; set forced recovery output |
All events extend AiProviderRequestBaseEvent which provides: requestThreadId (UUID linking pre/post events), requestParentId (for nested/chained calls), metadata (arbitrary key-value store that carries from pre to post), providerId, operationType, configuration, input, modelId, tags, debugData.
PreGenerateResponseEvent — Advanced Capabilities
| Method | Purpose |
|---|---|
setAuthentication($auth) |
Override provider authentication at runtime (e.g., per-user API keys) |
setForcedOutputObject(OutputInterface $output) |
Short-circuit the provider call entirely — return a cached or default response |
getForcedOutputObject() |
Check if another subscriber forced an output |
setMetadata($key, $value) |
Store metadata that passes through to PostGenerateResponseEvent |
PostStreamingResponseEvent
Fires after a streamed response completes. Extends AiProviderResponseBaseEvent (same as PostGenerateResponseEvent). Use getRequestThreadId() to correlate with the original PostGenerateResponseEvent. This event is read-only — it exists for collecting final results of streamed responses, not for modification.
AiExceptionEvent (Changed in 1.4)
Fired by ProviderProxy immediately before re-throwing a caught provider exception. Subscribers can:
- Rewrite the exception message via
setMessage()(useful for user-friendly error messages) - Inject a recovery output via
setForcedOutputObject(OutputInterface $output)— the proxy uses this instead of re-throwing (enables fallback providers or cached responses)
use Drupal\ai\Event\AiExceptionEvent;
public function onAiException(AiExceptionEvent $event): void {
if ($event->getException() instanceof AiRateLimitException) {
// Serve a cached response from backup storage
$event->setForcedOutputObject($cachedOutput);
}
}
public static function getSubscribedEvents(): array {
return [AiExceptionEvent::class => ['onAiException', 0]];
}
Event Properties
| Method | Pre | Post | PostStreaming | Description |
|---|---|---|---|---|
getInput() / setInput() |
R/W | R | R | Operation input |
getOutput() |
-- | R/W | R | Operation output |
getOperationType() |
R | R | R | Type string (chat, etc.) |
getProviderId() |
R | R | R | Provider plugin ID |
getModelId() |
R | R | R | Model identifier |
getTags() / setTags() |
R/W | R | R | Request tags array |
getConfiguration() / setConfiguration() |
R/W | R | R | Provider config |
getRequestThreadId() |
R | R | R | UUID linking pre/post events |
getRequestParentId() |
R | R | R | Parent request UUID (chained calls) |
getMetadata($key) / setMetadata($key, $val) |
R/W | R/W | R | Arbitrary metadata store |
getDebugData() / setDebugData($key, $val) |
R/W | R/W | R | Debug data |
setAuthentication($auth) |
R/W | -- | -- | Override authentication at runtime |
setForcedOutputObject($output) |
R/W | -- | -- | Short-circuit the provider call |
Tagging Convention
$provider->chat($input, $model, [
'my_module', // Module tag
'my_module:feature:summarize', // Feature tag
'my_module:entity_type:node', // Entity type
'my_module:bundle:article', // Bundle
]);
Tags enable: logging filters, guardrail targeting, event subscriber filtering, cost attribution.
Common Mistakes
- Wrong: Not using
getRequestThreadId()to correlate pre/post events → Right: UUID links pre and post events; use it for correlated logging - Wrong: Modifying output in
PostStreamingResponseEvent→ Right: This event is read-only — usePostGenerateResponseEventfor modification - Wrong: Not handling
AiExceptionEventfor rate limits → Right: Subscribe to inject fallback responses instead of surfacing API errors to users
See Also
- Guardrails System
- AI Observability
- Exceptions
- Reference:
web/modules/contrib/ai/src/Event/