[wp-trac] [WordPress Trac] #64938: wp_ai_client_prompt() does not pass the event dispatcher to the SDK PromptBuilder — before/after generate actions never fire
WordPress Trac
noreply at wordpress.org
Tue Mar 24 09:16:23 UTC 2026
#64938: wp_ai_client_prompt() does not pass the event dispatcher to the SDK
PromptBuilder — before/after generate actions never fire
-------------------------+-------------------------------------------------
Reporter: PerS | Owner: (none)
Type: defect | Status: new
(bug) |
Priority: normal | Milestone: Awaiting Review
Component: AI | Version: trunk
Severity: normal | Keywords: event-dispatcher has-patch ai-
Focuses: | client
-------------------------+-------------------------------------------------
=== Summary ===
`wp_ai_client_prompt()` in `wp-includes/ai-client.php` creates
`WP_AI_Client_Prompt_Builder` without passing the global event dispatcher.
As a result, the `wp_ai_client_before_generate_result` and
`wp_ai_client_after_generate_result` actions **never fire**, even though
`wp-settings.php` correctly configures a dispatcher via
`AiClient::setEventDispatcher()`.
=== Steps to Reproduce ===
1. Register callbacks on the before/after actions:
{{{#!php
add_action( 'wp_ai_client_before_generate_result', function ( $event ) {
error_log( 'before_generate fired' );
}, 10, 1 );
add_action( 'wp_ai_client_after_generate_result', function ( $event ) {
$usage = $event->getResult()->getTokenUsage();
error_log( sprintf( 'after_generate fired — %d tokens',
$usage->getTotalTokens() ) );
}, 10, 1 );
}}}
2. Trigger an AI prompt:
{{{#!php
$result = wp_ai_client_prompt( 'Summarize this text.' )
->set_model( 'gpt-4.1' )
->as_text()
->get();
}}}
3. Check the error log.
=== Expected Result ===
Both `before_generate` and `after_generate` should fire. The log should
show:
{{{
before_generate fired
after_generate fired — 42 tokens
}}}
=== Actual Result ===
Neither action fires. The AI request itself completes successfully (the
return value is correct), but no events are dispatched.
The `wp_ai_client_prevent_prompt` filter **does** fire correctly, because
it is called directly in `wp_ai_client_prompt()` via `apply_filters()`.
=== Root Cause ===
In `wp-includes/ai-client.php`, the builder is constructed with only two
arguments:
{{{#!php
// wp-includes/ai-client.php — wp_ai_client_prompt()
$builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry(),
$prompt );
}}}
The underlying SDK `PromptBuilder` constructor accepts an optional third
parameter:
{{{#!php
// vendor/wordpress/ai-client/src/PromptBuilder.php
public function __construct(
ProviderRegistryInterface $registry,
string $prompt,
?EventDispatcherInterface $eventDispatcher = null, // <-- never
passed
)
}}}
Because `$eventDispatcher` stays `null`, the SDK's internal
`dispatchEvent()` method silently skips all event dispatching:
{{{#!php
// PromptBuilder.php
private function dispatchEvent( object $event ): void {
if ( null !== $this->eventDispatcher ) {
$this->eventDispatcher->dispatch( $event );
}
}
}}}
Meanwhile, `wp-settings.php` **does** configure a global dispatcher:
{{{#!php
// wp-settings.php
AiClient::setEventDispatcher( new WP_AI_Client_Event_Dispatcher() );
}}}
This dispatcher is available via `AiClient::getEventDispatcher()`, but it
is never plumbed into the `PromptBuilder`.
=== Proposed Fix ===
Pass the event dispatcher as the third argument to
`WP_AI_Client_Prompt_Builder` (which should forward it to the SDK
`PromptBuilder`):
{{{#!diff
--- a/wp-includes/ai-client.php
+++ b/wp-includes/ai-client.php
@@ wp_ai_client_prompt()
- $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry(),
$prompt );
+ $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry(),
$prompt, AiClient::getEventDispatcher() );
}}}
If `WP_AI_Client_Prompt_Builder.__construct()` does not yet accept or
forward the dispatcher, it needs to be updated to pass it through to the
SDK `PromptBuilder`.
=== Current Workaround ===
Plugins can inject the dispatcher via reflection at an early priority on
`wp_ai_client_prevent_prompt`. The `prevent_prompt` filter receives a
clone of the builder, but since `WP_AI_Client_Prompt_Builder` has no
`__clone()` method, the clone shares the same inner SDK `PromptBuilder`
object — modifying it propagates to the original.
{{{#!php
use WordPress\AiClient\AiClient;
add_filter( 'wp_ai_client_prevent_prompt', function ( bool $prevent,
WP_AI_Client_Prompt_Builder $builder ): bool {
try {
$wp_ref = new ReflectionClass( $builder );
$builder_prop = $wp_ref->getProperty( 'builder' );
$sdk_builder = $builder_prop->getValue( $builder );
$sdk_ref = new ReflectionClass( $sdk_builder );
$disp_prop = $sdk_ref->getProperty( 'eventDispatcher' );
$current = $disp_prop->getValue( $sdk_builder );
if ( null === $current ) {
$dispatcher = AiClient::getEventDispatcher();
if ( null !== $dispatcher ) {
$disp_prop->setValue( $sdk_builder, $dispatcher );
}
}
} catch ( \Throwable ) {
// Reflection failed — don't break the request.
}
return $prevent;
}, 5, 2 );
}}}
This workaround is safe: once core passes the dispatcher properly, the
`null === $current` check causes the reflection to be skipped entirely.
=== Impact ===
Any plugin relying on `wp_ai_client_before_generate_result` or
`wp_ai_client_after_generate_result` to track token usage, log AI
requests, enforce budgets, or meter AI traffic is completely non-
functional in WP 7.0 without the reflection workaround.
=== Environment ===
* WordPress 7.0 (trunk as of 2026-03-24)
* PHP 8.3
* Tested with `azure_openai` provider, `gpt-4.1` model
--
Ticket URL: <https://core.trac.wordpress.org/ticket/64938>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform
More information about the wp-trac
mailing list