-
Notifications
You must be signed in to change notification settings - Fork 3.3k
fix: add wp_supports_ai() and related constant + filter
#11149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Changes from all commits
7ff6888
26d6c92
3aa0aae
941cc8b
78b010b
a5cef8e
98b881f
e47bedb
3ee6aaa
d4b8da5
a2d738f
95a827a
b3f04fd
f7f754d
5a06aea
2b05524
ffc7e32
414588a
d0b4a03
007b572
bb965aa
eda3323
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 check gives a hard short-circuit for environments that cannot be overridden with a filter, such as wp-config.php settings or hosting provider configurations. | ||||||||||
| if ( defined( 'WP_AI_SUPPORT' ) && ! WP_AI_SUPPORT ) { | ||||||||||
| return false; | ||||||||||
| } | ||||||||||
|
Comment on lines
+21
to
+23
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with this, and it's established in WordPress. Relying on constants without allowing to filter them is problematic for several reasons, including testing. We shouldn't avoid this pattern because we're scared of some malicious actor enabling AI again via filter. If you have a malicious actor that can do that, you have worse problems.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, but weaponization of AI swarms is more than just about immediately API key bills, when you think about the number of abandoned WP sites and our responsibility . And there are dozens of constants in core that have no matching filter.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Regarding testing, that's the reason the function has a filter. So we just not getting coverage of a single early return. (Conversely, there's no reason to add a constant at all if it's just to serve a default. You can accomplish the same thing with a different-priority hook callback) Will also repeat the argument that plugins shouldn't be able to override user choice on this, now that we're sub thread and not top-level any more. |
||||||||||
|
|
||||||||||
| /** | ||||||||||
| * 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 ); | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
| } | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Creates a new AI prompt builder using the default provider registry. | ||||||||||
| * | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,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. | ||
|
|
@@ -165,17 +167,22 @@ 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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @justlevine When reviewing, I found PHPStan complaining about |
||
| * 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 { | ||
| if ( ! wp_supports_ai() ) { | ||
| // The catch block will convert this to a WP_Error. | ||
| throw new \RuntimeException( __( 'AI features are not supported in this environment.' ) ); | ||
| } | ||
|
|
||
| $this->builder = new PromptBuilder( $registry, $prompt ); | ||
| } catch ( Exception $e ) { | ||
| $this->builder = new PromptBuilder( $registry ); | ||
|
|
@@ -290,26 +297,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( | ||
| 'exception_class' => 'WP_AI_Client_Prompt_Prevented', | ||
| ) | ||
|
|
@@ -385,7 +401,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. */ | ||
|
|
@@ -396,7 +413,7 @@ protected function get_builder_callable( string $name ): callable { | |
| ); | ||
| } | ||
|
|
||
| return array( $this->builder, $camel_case_name ); | ||
justlevine marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return $method; | ||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -192,6 +192,51 @@ 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 ); | ||
| } | ||
|
Comment on lines
+196
to
+199
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added this even though there's a short-circuit on the registry because this all seems to be hooked onto |
||
|
|
||
| /** | ||
| * Fires when the connector registry is ready for plugins to register connectors. | ||
| * | ||
| * Default connectors have already been registered at this point and cannot be | ||
| * unhooked. Use `$registry->register()` within this action to add new connectors. | ||
| * | ||
| * Example usage: | ||
| * | ||
| * add_action( 'wp_connectors_init', function ( WP_Connector_Registry $registry ) { | ||
| * $registry->register( | ||
| * 'my_custom_ai', | ||
| * array( | ||
| * 'name' => __( 'My Custom AI', 'my-plugin' ), | ||
| * 'description' => __( 'Custom AI provider integration.', 'my-plugin' ), | ||
| * 'type' => 'ai_provider', | ||
| * 'authentication' => array( | ||
| * 'method' => 'api_key', | ||
| * 'credentials_url' => 'https://example.com/api-keys', | ||
| * ), | ||
| * ) | ||
| * ); | ||
| * } ); | ||
| * | ||
| * @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( | ||
|
|
@@ -290,35 +335,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. | ||
| * | ||
| * Default connectors have already been registered at this point and cannot be | ||
| * unhooked. Use `$registry->register()` within this action to add new connectors. | ||
| * | ||
| * Example usage: | ||
| * | ||
| * add_action( 'wp_connectors_init', function ( WP_Connector_Registry $registry ) { | ||
| * $registry->register( | ||
| * 'my_custom_ai', | ||
| * array( | ||
| * 'name' => __( 'My Custom AI', 'my-plugin' ), | ||
| * 'description' => __( 'Custom AI provider integration.', 'my-plugin' ), | ||
| * 'type' => 'ai_provider', | ||
| * 'authentication' => array( | ||
| * 'method' => 'api_key', | ||
| * 'credentials_url' => 'https://example.com/api-keys', | ||
| * ), | ||
| * ) | ||
| * ); | ||
| * } ); | ||
| * | ||
| * @since 7.0.0 | ||
| * | ||
| * @param WP_Connector_Registry $registry Connector registry instance. | ||
| */ | ||
| do_action( 'wp_connectors_init', $registry ); | ||
| } | ||
|
|
||
| /** | ||
|
|
||
| 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() ); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.