Make WordPress Core

Changeset 62067


Ignore:
Timestamp:
03/19/2026 01:55:45 PM (9 days ago)
Author:
SergeyBiryukov
Message:

AI: Introduce wp_supports_ai() function to control LLM-related features.

This includes a WP_AI_SUPPORT constant and a wp_supports_ai filter.

When false,

  • _wp_connectors_get_provider_settings() will return an empty array (short-circuiting the other functionality).
  • WP_AI_Client_Prompt_Builder() will short-circuit the construction with a WP_Error(). wp_ai_client_prompt() will still return an instance, to allow for fluidity to remain intact.

Priority: WP_AI_SUPPORT > add_filter( 'wp_supports_ai', ...) > (default: true)

Follow-up to [61943], [61749], [61943].

Props justlevine, westonruter, gziolo, flixos90, romainmrhenry, ahortin, chrismcelroyseo, SergeyBiryukov.
See #64706.

Location:
trunk
Files:
1 added
6 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/ai-client.php

    r61700 r62067  
    99
    1010use WordPress\AiClient\AiClient;
     11
     12/**
     13 * Returns whether AI features are supported in the current environment.
     14 *
     15 * @since 7.0.0
     16 *
     17 * @return bool Whether AI features are supported.
     18 */
     19function wp_supports_ai(): bool {
     20    $is_enabled = defined( 'WP_AI_SUPPORT' ) ? WP_AI_SUPPORT : true;
     21
     22    /**
     23     * Filters whether the current request should use AI.
     24     *
     25     * This allows plugins and 3rd-party code to disable AI features on a per-request basis, or to even override explicit
     26     * preferences defined by the site owner.
     27     *
     28     * @since 7.0.0
     29     *
     30     * @param bool $is_enabled Whether the current request should use AI. Default to WP_AI_SUPPORT constant, or true if
     31     *                         the constant is not defined.
     32     */
     33    return (bool) apply_filters( 'wp_supports_ai', $is_enabled );
     34}
    1135
    1236/**
  • trunk/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php

    r62037 r62067  
    4646 *
    4747 * @since 7.0.0
     48 *
     49 * @phpstan-import-type Prompt from PromptBuilder
    4850 *
    4951 * @method self with_text(string $text) Adds text to the current message.
     
    171173     * @since 7.0.0
    172174     *
    173      * @param ProviderRegistry                                                                 $registry The provider registry for finding suitable models.
    174      * @param string|MessagePart|Message|array|list<string|MessagePart|array>|list<Message>|null $prompt   Optional. Initial prompt content.
    175      *                                                                                                    A string for simple text prompts,
    176      *                                                                                                    a MessagePart or Message object for
    177      *                                                                                                    structured content, an array for a
    178      *                                                                                                    message array shape, or a list of
    179      *                                                                                                    parts or messages for multi-turn
    180      *                                                                                                    conversations. Default null.
     175     * @param ProviderRegistry $registry The provider registry for finding suitable models.
     176     * @param Prompt          $prompt   Optional. Initial prompt content.
     177     *                                   A string for simple text prompts,
     178     *                                   a MessagePart or Message object for
     179     *                                   structured content, an array for a
     180     *                                   message array shape, or a list of
     181     *                                   parts or messages for multi-turn
     182     *                                   conversations. Default null.
    181183     */
    182184    public function __construct( ProviderRegistry $registry, $prompt = null ) {
     
    290292        // Check if the prompt should be prevented for is_supported* and generate_*/convert_text_to_speech* methods.
    291293        if ( self::is_support_check_method( $name ) || self::is_generating_method( $name ) ) {
    292             /**
    293              * Filters whether to prevent the prompt from being executed.
    294              *
    295              * @since 7.0.0
    296              *
    297              * @param bool                        $prevent Whether to prevent the prompt. Default false.
    298              * @param WP_AI_Client_Prompt_Builder $builder A clone of the prompt builder instance (read-only).
    299              */
    300             $prevent = (bool) apply_filters( 'wp_ai_client_prevent_prompt', false, clone $this );
     294            // If AI is not supported, then there's no need to apply the filter as the prompt will be prevented anyway.
     295            $is_ai_disabled = ! wp_supports_ai();
     296            $prevent        = $is_ai_disabled;
     297            if ( ! $prevent ) {
     298                /**
     299                 * Filters whether to prevent the prompt from being executed.
     300                 *
     301                 * @since 7.0.0
     302                 *
     303                 * @param bool                        $prevent Whether to prevent the prompt. Default false.
     304                 * @param WP_AI_Client_Prompt_Builder $builder A clone of the prompt builder instance (read-only).
     305                 */
     306                $prevent = (bool) apply_filters( 'wp_ai_client_prevent_prompt', false, clone $this );
     307            }
    301308
    302309            if ( $prevent ) {
     
    306313                }
    307314
     315                $error_message = $is_ai_disabled
     316                    ? __( 'AI features are not supported in this environment.' )
     317                    : __( 'Prompt execution was prevented by a filter.' );
     318
    308319                // For generate_* and convert_text_to_speech* methods, create a WP_Error.
    309320                $this->error = new WP_Error(
    310321                    'prompt_prevented',
    311                     __( 'Prompt execution was prevented by a filter.' ),
     322                    $error_message,
    312323                    array(
    313324                        'status' => 503,
     
    424435        $camel_case_name = $this->snake_to_camel_case( $name );
    425436
    426         if ( ! is_callable( array( $this->builder, $camel_case_name ) ) ) {
     437        $method = array( $this->builder, $camel_case_name );
     438        if ( ! is_callable( $method ) ) {
    427439            throw new BadMethodCallException(
    428440                sprintf(
     
    435447        }
    436448
    437         return array( $this->builder, $camel_case_name );
     449        return $method;
    438450    }
    439451
  • trunk/src/wp-includes/class-wp-connector-registry.php

    r62056 r62067  
    171171        }
    172172
     173        if ( 'ai_provider' === $args['type'] && ! wp_supports_ai() ) {
     174            // No need for a `doing_it_wrong` as AI support is disabled intentionally.
     175            return null;
     176        }
     177
    173178        $connector = array(
    174179            'name'           => $args['name'],
  • trunk/src/wp-includes/connectors.php

    r62032 r62067  
    11<?php
    22/**
    3  * Connectors API: core functions for registering and managing connectors.
    4  *
    5  * The Connectors API provides a unified framework for registering and managing
    6  * external service integrations within WordPress. A "connector" represents a
    7  * connection to an external service — currently focused on AI providers — with
    8  * standardized metadata, authentication configuration, and plugin association.
    9  *
    10  * ## Overview
    11  *
    12  * The Connectors API enables developers to:
    13  *
    14  *  - Register AI provider connectors with standardized interfaces.
    15  *  - Define authentication methods and credential sources.
    16  *  - Associate connectors with WordPress.org plugins for install/activate UI.
    17  *  - Expose connector settings through the REST API with automatic key masking.
    18  *
    19  * ## AI Provider Plugins
    20  *
    21  * AI provider plugins that register with the WP AI Client's `ProviderRegistry`
    22  * get automatic connector integration — no explicit connector registration is
    23  * needed. The system discovers providers from the WP AI Client registry and
    24  * creates connectors with the correct name, description, logo, authentication
    25  * method, and setting name derived from the provider's configuration.
    26  *
    27  * The authentication method (`api_key` or `none`) is determined by the provider's
    28  * metadata in the WP AI Client. For `api_key` providers, a `setting_name` is
    29  * automatically generated following the same naming convention used for environment
    30  * variables and PHP constants (e.g., provider `openai` maps to `OPENAI_API_KEY`
    31  * for env/constant lookup).
    32  *
    33  * @see WordPress\AiClient\Providers\ProviderRegistry
    34  *
    35  * ## Admin UI Integration
    36  *
    37  * Registered `ai_provider` connectors appear on the Settings → Connectors
    38  * admin screen. The screen renders each connector as a card using the
    39  * registry data:
    40  *
    41  *  - `name`, `description`, and `logo_url` are displayed on the card.
    42  *  - `plugin.slug` enables install/activate controls — the screen checks
    43  *    whether the plugin is installed and active, and shows the appropriate
    44  *    action button.
    45  *  - `authentication.credentials_url` is rendered as a link directing users
    46  *    to the provider's site to obtain API credentials.
    47  *  - For `api_key` connectors, the screen shows the current key source
    48  *    (environment variable, PHP constant, or database) and connection status.
    49  *
    50  * On the backend, `api_key` connectors also receive automatic settings
    51  * registration via the Settings API (`show_in_rest`), API key masking in
    52  * REST API responses, and key validation against the provider on update.
    53  *
    54  * Connectors with other authentication methods or types are registered in the PHP
    55  * registry and exposed via the script module data, but require a client-side
    56  * JavaScript registration for custom frontend UI. Support for additional
    57  * authentication methods and connector types is planned for future releases.
    58  *
    59  * ## Custom Connectors
    60  *
    61  * The `wp_connectors_init` action hook allows plugins to override metadata on
    62  * existing connectors. AI provider connectors are auto-discovered from the WP
    63  * AI Client registry and should not be manually registered here.
    64  *
    65  * Example — overriding the description of an auto-discovered connector:
    66  *
    67  *     add_action( 'wp_connectors_init', function ( WP_Connector_Registry $registry ) {
    68  *         if ( $registry->is_registered( 'openai' ) ) {
    69  *             $connector = $registry->unregister( 'openai' );
    70  *             $connector['description'] = __( 'Custom description for OpenAI.', 'my-plugin' );
    71  *             $registry->register( 'openai', $connector );
    72  *         }
    73  *     } );
    74  *
    75  * Non-AI-provider connector types are not yet fully supported. The PHP registry
    76  * accepts any connector type, but only `ai_provider` connectors with `api_key`
    77  * authentication receive automatic admin UI. Support for additional connector
    78  * types with dedicated frontend integration is planned for future releases.
    79  * When available, this action will be the primary hook for registering those
    80  * new connector types.
    81  *
    82  * ## Initialization Lifecycle
    83  *
    84  * During `init`, the system:
    85  *
    86  *  1. Creates the `WP_Connector_Registry` singleton.
    87  *  2. Registers built-in connectors (Anthropic, Google, OpenAI) with hardcoded defaults.
    88  *  3. Auto-discovers providers from the WP AI Client registry and merges their
    89  *     metadata (name, description, logo, authentication) on top of defaults,
    90  *     with registry values taking precedence.
    91  *  4. Fires the `wp_connectors_init` action so plugins can override metadata
    92  *     on existing connectors or register additional connectors.
    93  *  5. Registers settings and passes stored API keys to the WP AI Client.
    94  *
    95  * ## Authentication
    96  *
    97  * Connectors support two authentication methods:
    98  *
    99  *  - `api_key`: Requires an API key, which can be provided via environment variable,
    100  *    PHP constant, or the database (checked in that order).
    101  *  - `none`: No authentication required.
    102  *
    103  * API keys stored in the database are automatically masked in REST API responses
    104  * and validated against the provider on update.
     3 * Connectors API.
    1054 *
    1065 * @package WordPress
     
    11514 * Checks if a connector is registered.
    11615 *
    117  * Example:
    118  *
    119  *     if ( wp_is_connector_registered( 'openai' ) ) {
    120  *         // The OpenAI connector is available.
    121  *     }
    122  *
    12316 * @since 7.0.0
    12417 *
    12518 * @see WP_Connector_Registry::is_registered()
    126  * @see wp_get_connector()
    127  * @see wp_get_connectors()
    12819 *
    12920 * @param string $id The connector identifier.
     
    14233 * Retrieves a registered connector.
    14334 *
    144  * Example:
    145  *
    146  *     $connector = wp_get_connector( 'openai' );
    147  *     if ( $connector ) {
    148  *         echo $connector['name']; // 'OpenAI'
    149  *     }
    150  *
    15135 * @since 7.0.0
    15236 *
    15337 * @see WP_Connector_Registry::get_registered()
    154  * @see wp_is_connector_registered()
    155  * @see wp_get_connectors()
    15638 *
    15739 * @param string $id The connector identifier.
     
    20486 * Retrieves all registered connectors.
    20587 *
    206  * Example:
    207  *
    208  *     $connectors = wp_get_connectors();
    209  *     foreach ( $connectors as $id => $connector ) {
    210  *         printf( '%s: %s', $connector['name'], $connector['description'] );
    211  *     }
    212  *
    21388 * @since 7.0.0
    21489 *
    21590 * @see WP_Connector_Registry::get_all_registered()
    216  * @see wp_is_connector_registered()
    217  * @see wp_get_connector()
    21891 *
    21992 * @return array {
     
    311184 * Initializes the connector registry with default connectors and fires the registration action.
    312185 *
    313  * This function orchestrates the full connector initialization sequence:
    314  *
    315  *  1. Creates the `WP_Connector_Registry` singleton instance.
    316  *  2. Defines built-in connectors (Anthropic, Google, OpenAI) with hardcoded defaults
    317  *     including name, description, type, plugin slug, and authentication configuration.
    318  *  3. Merges metadata from the WP AI Client provider registry on top of defaults.
    319  *     Registry values (from provider plugins) take precedence over hardcoded fallbacks
    320  *     for name, description, logo URL, and authentication method.
    321  *  4. Registers all connectors (built-in and AI Client-discovered) on the registry.
    322  *  5. Fires the `wp_connectors_init` action for plugins to override metadata
    323  *     on existing connectors or register additional connectors.
    324  *
    325  * Built-in connectors are registered before the action fires and cannot be unhooked.
    326  * Plugins should use the `wp_connectors_init` action to override metadata or
    327  * register new connectors via `$registry->register()`.
     186 * Creates the registry instance, registers built-in connectors (which cannot be unhooked),
     187 * and then fires the `wp_connectors_init` action for plugins to register their own connectors.
    328188 *
    329189 * @since 7.0.0
     
    333193    $registry = new WP_Connector_Registry();
    334194    WP_Connector_Registry::set_instance( $registry );
     195
     196    // Only register default AI providers if AI support is enabled.
     197    if ( wp_supports_ai() ) {
     198        _wp_connectors_register_default_ai_providers( $registry );
     199    }
     200
     201    /**
     202     * Fires when the connector registry is ready for plugins to register connectors.
     203     *
     204     * Built-in connectors and any AI providers auto-discovered from the WP AI Client
     205     * registry have already been registered at this point and cannot be unhooked.
     206     *
     207     * AI provider plugins that register with the WP AI Client do not need to use
     208     * this action — their connectors are created automatically. This action is
     209     * primarily for registering non-AI-provider connectors or overriding metadata
     210     * on existing connectors.
     211     *
     212     * Use `$registry->register()` within this action to add new connectors.
     213     * To override an existing connector, unregister it first, then re-register
     214     * with updated data.
     215     *
     216     * Example — overriding metadata on an auto-discovered connector:
     217     *
     218     *     add_action( 'wp_connectors_init', function ( WP_Connector_Registry $registry ) {
     219     *         if ( $registry->is_registered( 'openai' ) ) {
     220     *             $connector = $registry->unregister( 'openai' );
     221     *             $connector['description'] = __( 'Custom description for OpenAI.', 'my-plugin' );
     222     *             $registry->register( 'openai', $connector );
     223     *         }
     224     *     } );
     225     *
     226     * @since 7.0.0
     227     *
     228     * @param WP_Connector_Registry $registry Connector registry instance.
     229     */
     230    do_action( 'wp_connectors_init', $registry );
     231}
     232
     233/**
     234 * Registers connectors for the built-in AI providers.
     235 *
     236 * @since 7.0.0
     237 * @access private
     238 *
     239 * @param WP_Connector_Registry $registry The connector registry instance.
     240 */
     241function _wp_connectors_register_default_ai_providers( WP_Connector_Registry $registry ): void {
    335242    // Built-in connectors.
    336243    $defaults = array(
     
    431338        $registry->register( $id, $args );
    432339    }
    433 
    434     /**
    435      * Fires when the connector registry is ready for plugins to register connectors.
    436      *
    437      * Built-in connectors and any AI providers auto-discovered from the WP AI Client
    438      * registry have already been registered at this point and cannot be unhooked.
    439      *
    440      * AI provider plugins that register with the WP AI Client do not need to use
    441      * this action — their connectors are created automatically. This action is
    442      * primarily for registering non-AI-provider connectors or overriding metadata
    443      * on existing connectors.
    444      *
    445      * Use `$registry->register()` within this action to add new connectors.
    446      * To override an existing connector, unregister it first, then re-register
    447      * with updated data.
    448      *
    449      * Example — overriding metadata on an auto-discovered connector:
    450      *
    451      *     add_action( 'wp_connectors_init', function ( WP_Connector_Registry $registry ) {
    452      *         if ( $registry->is_registered( 'openai' ) ) {
    453      *             $connector = $registry->unregister( 'openai' );
    454      *             $connector['description'] = __( 'Custom description for OpenAI.', 'my-plugin' );
    455      *             $registry->register( 'openai', $connector );
    456      *         }
    457      *     } );
    458      *
    459      * @since 7.0.0
    460      *
    461      * @param WP_Connector_Registry $registry Connector registry instance.
    462      */
    463     do_action( 'wp_connectors_init', $registry );
    464340}
    465341
     
    628504/**
    629505 * Registers default connector settings.
    630  *
    631  * Only registers settings for `ai_provider` connectors with `api_key`
    632  * authentication whose provider is present in the WP AI Client registry.
    633  * Each setting is registered with `show_in_rest` enabled, making it
    634  * accessible through the `/wp/v2/settings` REST endpoint.
    635506 *
    636507 * @since 7.0.0
     
    721592
    722593/**
    723  * Provides connector data to the Settings → Connectors admin screen.
    724  *
    725  * This function is the bridge between the PHP connector registry and the
    726  * frontend admin UI. It transforms each registered connector into the data
    727  * structure consumed by the `options-connectors-wp-admin` script module,
    728  * enriching registry data with runtime state:
    729  *
    730  *  - Plugin install/activate status (via `get_plugins()` and `is_plugin_active()`).
    731  *  - API key source detection (`env`, `constant`, `database`, or `none`).
    732  *  - Connection status for `api_key` connectors (via the WP AI Client registry).
    733  *
    734  * Hooked to the `script_module_data_options-connectors-wp-admin` filter.
    735  *
    736  * @since 7.0.0
    737  * @access private
    738  *
    739  * @see _wp_connectors_get_api_key_source()
     594 * Exposes connector settings to the connectors-wp-admin script module.
     595 *
     596 * @since 7.0.0
     597 * @access private
    740598 *
    741599 * @param array<string, mixed> $data Existing script module data.
    742  * @return array<string, mixed> Script module data with a `connectors` key added,
    743  *                              keyed by connector ID and sorted alphabetically.
     600 * @return array<string, mixed> Script module data with connectors added.
    744601 */
    745602function _wp_connectors_get_connector_script_module_data( array $data ): array {
  • trunk/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php

    r62037 r62067  
    24062406     * @ticket 64591
    24072407     */
     2408    public function test_is_supported_returns_false_when_ai_not_supported() {
     2409        add_filter( 'wp_supports_ai', '__return_false' );
     2410
     2411        $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry(), 'Test prompt' );
     2412
     2413        $this->assertFalse( $builder->is_supported() );
     2414    }
     2415
     2416    /**
     2417     * Tests that is_supported returns false when prevent prompt filter returns true.
     2418     *
     2419     * @ticket 64591
     2420     */
    24082421    public function test_is_supported_returns_false_when_filter_prevents_prompt() {
    24092422        add_filter( 'wp_ai_client_prevent_prompt', '__return_true' );
     
    24132426        $this->assertFalse( $builder->is_supported() );
    24142427    }
    2415 
    24162428    /**
    24172429     * Tests that generate_result returns WP_Error when prevent prompt filter returns true.
  • trunk/tests/phpunit/tests/connectors/wpConnectorRegistry.php

    r62056 r62067  
    398398        $this->assertSame( $instance1, $instance2 );
    399399    }
     400
     401    /**
     402     * Test registration skips AI connectors when AI is not supported.
     403     */
     404    public function test_register_skips_when_ai_not_supported() {
     405        add_filter( 'wp_supports_ai', '__return_false' );
     406
     407        $this->registry->register( 'first', self::$default_args );
     408
     409        $all = $this->registry->get_all_registered();
     410        $this->assertCount( 0, $all );
     411    }
    400412}
Note: See TracChangeset for help on using the changeset viewer.