Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
7ff6888
fix: add `wp_supports_ai()` and related constant + filter
justlevine Mar 4, 2026
26d6c92
Merge remote-tracking branch 'upstream' into fix/wp_supports_ai
justlevine Mar 4, 2026
3aa0aae
fix: resolve merge conflicts
justlevine Mar 4, 2026
941cc8b
dev: rename const to WP_AI_SUPPORT
justlevine Mar 4, 2026
78b010b
tests: gate `ReflectionProperty::setAccessible()`
justlevine Mar 4, 2026
a5cef8e
Apply suggestions from code review
justlevine Mar 7, 2026
98b881f
chore: phpcbf
justlevine Mar 7, 2026
e47bedb
Merge branch 'trunk' into fix/wp_supports_ai
justlevine Mar 11, 2026
3ee6aaa
chore: feedback
justlevine Mar 11, 2026
d4b8da5
Merge branch 'trunk' into fix/wp_supports_ai
justlevine Mar 11, 2026
a2d738f
Reuse Prompt type from PromptBuilder in WP_AI_Client_Prompt_Builder c…
westonruter Mar 11, 2026
95a827a
Fix PHPStan error about non-callable being returned
westonruter Mar 11, 2026
b3f04fd
Add type hint for _wp_connectors_register_default_ai_providers()
westonruter Mar 11, 2026
f7f754d
Remove blank line
westonruter Mar 11, 2026
5a06aea
Add void return types
westonruter Mar 11, 2026
2b05524
Remove needless assertion since _wp_connectors_get_connector_settings…
westonruter Mar 11, 2026
ffc7e32
Merge branch 'trunk' into fix/wp_supports_ai
justlevine Mar 12, 2026
414588a
Merge branch 'trunk' into fix/wp_supports_ai
justlevine Mar 12, 2026
d0b4a03
chore: lint after merging
justlevine Mar 12, 2026
007b572
chore: fix test and cleanup
justlevine Mar 12, 2026
bb965aa
Merge branch 'trunk' into fix/wp_supports_ai
justlevine Mar 12, 2026
eda3323
fix: check for support in __call()
justlevine Mar 12, 2026
e0719a0
Merge branch 'trunk' into fix/wp_supports_ai
justlevine Mar 16, 2026
e8387df
chore: post merge cleanup
justlevine Mar 16, 2026
bd314e7
Merge branch 'trunk' into fix/wp_supports_ai
justlevine Mar 16, 2026
9773705
tests: test `Registry->get_all_registered()` instead of downstream
justlevine Mar 16, 2026
069e6d4
chore: revert `__call()` check in favor of constructor
justlevine Mar 16, 2026
4f43aad
chore: move support check to __call()
justlevine Mar 18, 2026
0d1ffb9
tests: remove unnecessary test
justlevine Mar 18, 2026
9126d8b
dev: allow filter to override constant
justlevine Mar 18, 2026
bcb364b
Merge branch 'trunk' into fix/wp_supports_ai
justlevine Mar 18, 2026
f0c1632
fix: prevent filter from overriding WP_AI_SUPPORT preference
justlevine Mar 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/wp-includes/ai-client.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,29 @@

use WordPress\AiClient\AiClient;

/**
* Returns whether AI features are supported in the current environment.
*
* @since 7.0.0
*
* @return bool Whether AI features are supported.
*/
function wp_supports_ai(): bool {
// Constant ensures 3rd-party code cannot override the explicit preferences defined by the environment.
if ( defined( 'WP_AI_SUPPORT' ) && ! WP_AI_SUPPORT ) {
return false;
}

/**
* Filters whether the current request should use AI.
*
* @since 7.0.0
*
* @param bool $is_enabled Whether the current request should use AI. Default true.
*/
return (bool) apply_filters( 'wp_supports_ai', true );
}

/**
* Creates a new AI prompt builder using the default provider registry.
*
Expand Down
52 changes: 32 additions & 20 deletions src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
*
* @since 7.0.0
*
* @phpstan-import-type Prompt from PromptBuilder
*
* @method self with_text(string $text) Adds text to the current message.
* @method self with_file($file, ?string $mimeType = null) Adds a file to the current message.
* @method self with_function_response(FunctionResponse $functionResponse) Adds a function response to the current message.
Expand Down Expand Up @@ -170,14 +172,14 @@ class WP_AI_Client_Prompt_Builder {
*
* @since 7.0.0
*
* @param ProviderRegistry $registry The provider registry for finding suitable models.
* @param string|MessagePart|Message|array|list<string|MessagePart|array>|list<Message>|null $prompt Optional. Initial prompt content.
* A string for simple text prompts,
* a MessagePart or Message object for
* structured content, an array for a
* message array shape, or a list of
* parts or messages for multi-turn
* conversations. Default null.
* @param ProviderRegistry $registry The provider registry for finding suitable models.
* @param Prompt $prompt Optional. Initial prompt content.
* A string for simple text prompts,
* a MessagePart or Message object for
* structured content, an array for a
* message array shape, or a list of
* parts or messages for multi-turn
* conversations. Default null.
*/
public function __construct( ProviderRegistry $registry, $prompt = null ) {
try {
Expand Down Expand Up @@ -289,26 +291,35 @@ public function __call( string $name, array $arguments ) {

// Check if the prompt should be prevented for is_supported* and generate_*/convert_text_to_speech* methods.
if ( self::is_support_check_method( $name ) || self::is_generating_method( $name ) ) {
/**
* Filters whether to prevent the prompt from being executed.
*
* @since 7.0.0
*
* @param bool $prevent Whether to prevent the prompt. Default false.
* @param WP_AI_Client_Prompt_Builder $builder A clone of the prompt builder instance (read-only).
*/
$prevent = (bool) apply_filters( 'wp_ai_client_prevent_prompt', false, clone $this );
// If AI is not supported, then there's no need to apply the filter as the prompt will be prevented anyway.
$is_ai_disabled = ! wp_supports_ai();
$prevent = $is_ai_disabled;
if ( ! $prevent ) {
/**
* Filters whether to prevent the prompt from being executed.
*
* @since 7.0.0
*
* @param bool $prevent Whether to prevent the prompt. Default false.
* @param WP_AI_Client_Prompt_Builder $builder A clone of the prompt builder instance (read-only).
*/
$prevent = (bool) apply_filters( 'wp_ai_client_prevent_prompt', false, clone $this );
}

if ( $prevent ) {
// For is_supported* methods, return false.
if ( self::is_support_check_method( $name ) ) {
return false;
}

$error_message = $is_ai_disabled
? __( 'AI features are not supported in this environment.' )
: __( 'Prompt execution was prevented by a filter.' );

// For generate_* and convert_text_to_speech* methods, create a WP_Error.
$this->error = new WP_Error(
'prompt_prevented',
__( 'Prompt execution was prevented by a filter.' ),
$error_message,
array(
'status' => 503,
)
Expand Down Expand Up @@ -423,7 +434,8 @@ private static function is_generating_method( string $name ): bool {
protected function get_builder_callable( string $name ): callable {
$camel_case_name = $this->snake_to_camel_case( $name );

if ( ! is_callable( array( $this->builder, $camel_case_name ) ) ) {
$method = array( $this->builder, $camel_case_name );
if ( ! is_callable( $method ) ) {
throw new BadMethodCallException(
sprintf(
/* translators: 1: Method name. 2: Class name. */
Expand All @@ -434,7 +446,7 @@ protected function get_builder_callable( string $name ): callable {
);
}

return array( $this->builder, $camel_case_name );
return $method;
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/wp-includes/class-wp-connector-registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ public function register( string $id, array $args ): ?array {
return null;
}

if ( 'ai_provider' === $args['type'] && ! wp_supports_ai() ) {
// No need for a `doing_it_wrong` as AI support is disabled intentionally.
return null;
}

$connector = array(
'name' => $args['name'],
'description' => isset( $args['description'] ) && is_string( $args['description'] ) ? $args['description'] : '',
Expand Down
78 changes: 47 additions & 31 deletions src/wp-includes/connectors.php
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,53 @@ function _wp_connectors_resolve_ai_provider_logo_url( string $path ): ?string {
function _wp_connectors_init(): void {
$registry = new WP_Connector_Registry();
WP_Connector_Registry::set_instance( $registry );

// Only register default AI providers if AI support is enabled.
if ( wp_supports_ai() ) {
_wp_connectors_register_default_ai_providers( $registry );
}

/**
* Fires when the connector registry is ready for plugins to register connectors.
*
* Built-in connectors and any AI providers auto-discovered from the WP AI Client
* registry have already been registered at this point and cannot be unhooked.
*
* AI provider plugins that register with the WP AI Client do not need to use
* this action — their connectors are created automatically. This action is
* primarily for registering non-AI-provider connectors or overriding metadata
* on existing connectors.
*
* Use `$registry->register()` within this action to add new connectors.
* To override an existing connector, unregister it first, then re-register
* with updated data.
*
* Example — overriding metadata on an auto-discovered connector:
*
* add_action( 'wp_connectors_init', function ( WP_Connector_Registry $registry ) {
* if ( $registry->is_registered( 'openai' ) ) {
* $connector = $registry->unregister( 'openai' );
* $connector['description'] = __( 'Custom description for OpenAI.', 'my-plugin' );
* $registry->register( 'openai', $connector );
* }
* } );
*
* @since 7.0.0
*
* @param WP_Connector_Registry $registry Connector registry instance.
*/
do_action( 'wp_connectors_init', $registry );
}

/**
* Registers connectors for the built-in AI providers.
*
* @since 7.0.0
* @access private
*
* @param WP_Connector_Registry $registry The connector registry instance.
*/
function _wp_connectors_register_default_ai_providers( WP_Connector_Registry $registry ): void {
// Built-in connectors.
$defaults = array(
'anthropic' => array(
Expand Down Expand Up @@ -430,37 +477,6 @@ function _wp_connectors_init(): void {
foreach ( $defaults as $id => $args ) {
$registry->register( $id, $args );
}

/**
* Fires when the connector registry is ready for plugins to register connectors.
*
* Built-in connectors and any AI providers auto-discovered from the WP AI Client
* registry have already been registered at this point and cannot be unhooked.
*
* AI provider plugins that register with the WP AI Client do not need to use
* this action — their connectors are created automatically. This action is
* primarily for registering non-AI-provider connectors or overriding metadata
* on existing connectors.
*
* Use `$registry->register()` within this action to add new connectors.
* To override an existing connector, unregister it first, then re-register
* with updated data.
*
* Example — overriding metadata on an auto-discovered connector:
*
* add_action( 'wp_connectors_init', function ( WP_Connector_Registry $registry ) {
* if ( $registry->is_registered( 'openai' ) ) {
* $connector = $registry->unregister( 'openai' );
* $connector['description'] = __( 'Custom description for OpenAI.', 'my-plugin' );
* $registry->register( 'openai', $connector );
* }
* } );
*
* @since 7.0.0
*
* @param WP_Connector_Registry $registry Connector registry instance.
*/
do_action( 'wp_connectors_init', $registry );
}

/**
Expand Down
14 changes: 13 additions & 1 deletion tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2400,6 +2400,19 @@ public function test_using_ability_method_chaining() {
$this->assertEquals( 500, $config->getMaxTokens() );
}

/**
* Tests that is_supported returns false when prevent prompt filter returns true.
*
* @ticket 64591
*/
public function test_is_supported_returns_false_when_ai_not_supported() {
add_filter( 'wp_supports_ai', '__return_false' );

$builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry(), 'Test prompt' );

$this->assertFalse( $builder->is_supported() );
}

/**
* Tests that is_supported returns false when prevent prompt filter returns true.
*
Expand All @@ -2412,7 +2425,6 @@ public function test_is_supported_returns_false_when_filter_prevents_prompt() {

$this->assertFalse( $builder->is_supported() );
}

/**
* Tests that generate_result returns WP_Error when prevent prompt filter returns true.
*
Expand Down
30 changes: 30 additions & 0 deletions tests/phpunit/tests/ai-client/wpSupportsAI.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php
/**
* Tests for wp_supports_ai().
*
* @group ai-client
* @covers ::wp_supports_ai
*/

class Tests_WP_Supports_AI extends WP_UnitTestCase {
/**
* Test that wp_supports_ai() defaults to true.
*
* @ticket 64591
*/
public function test_defaults_to_true(): void {
$this->assertTrue( wp_supports_ai() );
}

/**
* Tests that the wp_supports_ai filter can disable/enable AI features.
*/
public function test_filter_can_disable_ai_features(): void {
add_filter( 'wp_supports_ai', '__return_false' );
$this->assertFalse( wp_supports_ai() );

// Try a later filter to re-enable AI and confirm that it works.
add_filter( 'wp_supports_ai', '__return_true' );
$this->assertTrue( wp_supports_ai() );
}
}
12 changes: 12 additions & 0 deletions tests/phpunit/tests/connectors/wpConnectorRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -381,4 +381,16 @@ public function test_get_instance_returns_same_instance() {

$this->assertSame( $instance1, $instance2 );
}

/**
* Test registration skips AI connectors when AI is not supported.
*/
public function test_register_skips_when_ai_not_supported() {
add_filter( 'wp_supports_ai', '__return_false' );

$this->registry->register( 'first', self::$default_args );

$all = $this->registry->get_all_registered();
$this->assertCount( 0, $all );
}
}
Loading