diff --git a/src/wp-admin/menu.php b/src/wp-admin/menu.php
index e544175d153b4..a682f715ac88a 100644
--- a/src/wp-admin/menu.php
+++ b/src/wp-admin/menu.php
@@ -410,6 +410,9 @@ function _add_plugin_file_editor_to_tools() {
$submenu['options-general.php'][30] = array( __( 'Media' ), 'manage_options', 'options-media.php' );
$submenu['options-general.php'][40] = array( __( 'Permalinks' ), 'manage_options', 'options-permalink.php' );
$submenu['options-general.php'][45] = array( __( 'Privacy' ), 'manage_privacy_options', 'options-privacy.php' );
+if ( ! empty( $GLOBALS['wp_ai_client_credentials_manager']->get_all_cloud_providers_metadata() ) ) {
+ $submenu['options-general.php'][47] = array( __( 'AI Services' ), 'manage_options', 'options-ai.php' );
+}
$_wp_last_utility_menu = 80; // The index of the last top-level menu in the utility menu group.
diff --git a/src/wp-admin/options-ai.php b/src/wp-admin/options-ai.php
new file mode 100644
index 0000000000000..df625f1737d02
--- /dev/null
+++ b/src/wp-admin/options-ai.php
@@ -0,0 +1,102 @@
+get_all_cloud_providers_metadata();
+
+$settings_section = 'wp-ai-client-provider-credentials';
+
+add_settings_section(
+ $settings_section,
+ '',
+ static function () {
+ ?>
+
+
+
+ getId();
+ $provider_name = $provider_metadata->getName();
+ $provider_credentials_url = $provider_metadata->getCredentialsUrl();
+
+ $field_id = 'wp-ai-client-provider-api-key-' . $provider_id;
+ $field_args = array(
+ 'type' => 'password',
+ 'label_for' => $field_id,
+ 'id' => $field_id,
+ 'name' => WP_AI_Client_Credentials_Manager::OPTION_PROVIDER_CREDENTIALS . '[' . $provider_id . ']',
+ );
+ if ( $provider_credentials_url ) {
+ $field_args['description'] = sprintf(
+ /* translators: 1: AI provider name, 2: URL to the provider's API credentials page. */
+ __( 'Create and manage your %1$s API keys in the %1$s account settings (opens in a new tab) .' ),
+ $provider_name,
+ esc_url( $provider_credentials_url )
+ );
+ }
+
+ add_settings_field(
+ $field_id,
+ $provider_name,
+ 'wp_ai_client_render_credential_field',
+ 'ai',
+ $settings_section,
+ $field_args
+ );
+}
+
+$ai_help = '' . __( 'This screen allows you to configure API credentials for AI service providers. These credentials are used by AI-powered features throughout your site.' ) . '
';
+$ai_help .= '' . __( 'You must click the Save Changes button at the bottom of the screen for new settings to take effect.' ) . '
';
+
+get_current_screen()->add_help_tab(
+ array(
+ 'id' => 'overview',
+ 'title' => __( 'Overview' ),
+ 'content' => $ai_help,
+ )
+);
+
+get_current_screen()->set_help_sidebar(
+ '' . __( 'For more information:' ) . '
' .
+ '' . __( 'Support forums ' ) . '
'
+);
+
+require_once ABSPATH . 'wp-admin/admin-header.php';
+
+?>
+
+
+
+
+
+
+
+
+
diff --git a/src/wp-admin/options.php b/src/wp-admin/options.php
index 57c22be86d367..e297cdc7845ac 100644
--- a/src/wp-admin/options.php
+++ b/src/wp-admin/options.php
@@ -159,6 +159,7 @@
$allowed_options['misc'] = array();
$allowed_options['options'] = array();
$allowed_options['privacy'] = array();
+$allowed_options['ai'] = array();
/**
* Filters whether the post-by-email functionality is enabled.
diff --git a/src/wp-includes/ai-client.php b/src/wp-includes/ai-client.php
index 88a1fdf323f52..8d5a1db314617 100644
--- a/src/wp-includes/ai-client.php
+++ b/src/wp-includes/ai-client.php
@@ -32,3 +32,72 @@
function wp_ai_client_prompt( $prompt = null ) {
return new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry(), $prompt );
}
+
+/**
+ * Renders a credential input field for the AI Services settings page.
+ *
+ * @since 7.0.0
+ * @access private
+ *
+ * @param array $args {
+ * Field arguments set up during add_settings_field().
+ *
+ * @type string $type Input type. Default 'text'.
+ * @type string $id Field ID attribute.
+ * @type string $name Field name attribute, may include array notation.
+ * @type string $description Optional. Field description HTML.
+ * }
+ */
+function wp_ai_client_render_credential_field( $args ) {
+ $type = isset( $args['type'] ) ? $args['type'] : 'text';
+ $id = isset( $args['id'] ) ? $args['id'] : '';
+ $name = isset( $args['name'] ) ? $args['name'] : '';
+ $description = isset( $args['description'] ) ? $args['description'] : '';
+ $description_id = $id . '_description';
+
+ if ( str_contains( $name, '[' ) ) {
+ $parts = explode( '[', $name, 2 );
+ $option = get_option( $parts[0] );
+ $subkey = trim( $parts[1], ']' );
+ if ( is_array( $option ) && isset( $option[ $subkey ] ) && is_string( $option[ $subkey ] ) ) {
+ $value = $option[ $subkey ];
+ } else {
+ $value = '';
+ }
+ } else {
+ $option = get_option( $name );
+ $value = is_string( $option ) ? $option : '';
+ }
+
+ ?>
+
+ >
+ array(
+ 'class' => array(),
+ 'href' => array(),
+ 'target' => array(),
+ 'rel' => array(),
+ ),
+ 'strong' => array(),
+ 'em' => array(),
+ 'span' => array(
+ 'class' => array(),
+ ),
+ );
+ ?>
+
+
+
+ getRegisteredProviderIds();
+ foreach ( $provider_ids as $provider_id ) {
+ // If the provider was already found via another client class, just add this client class name.
+ if ( isset( $wp_ai_client_providers_metadata[ $provider_id ] ) ) {
+ if ( ! is_array( $wp_ai_client_providers_metadata[ $provider_id ]['ai_client_classnames'] ) ) {
+ _doing_it_wrong(
+ __METHOD__,
+ __( 'Invalid format for collected provider AI client class names.' ),
+ '7.0.0'
+ );
+ continue;
+ }
+ $wp_ai_client_providers_metadata[ $provider_id ]['ai_client_classnames'][ AiClient::class ] = true;
+ continue;
+ }
+
+ // Get the provider metadata and add it to the global.
+ $provider_class_name = $registry->getProviderClassName( $provider_id );
+ $provider_metadata = $provider_class_name::metadata();
+
+ $wp_ai_client_providers_metadata[ $provider_id ] = array_merge(
+ $provider_metadata->toArray(),
+ array(
+ 'ai_client_classnames' => array( AiClient::class => true ),
+ )
+ );
+ }
+ }
+
+ /**
+ * Returns the metadata for all registered providers across all instances of the PHP AI Client SDK.
+ *
+ * @since 7.0.0
+ *
+ * @return array Array of provider metadata objects,
+ * keyed by provider ID.
+ */
+ public function get_all_providers_metadata() {
+ global $wp_ai_client_providers_metadata;
+
+ if ( ! isset( $wp_ai_client_providers_metadata ) ) {
+ $wp_ai_client_providers_metadata = array();
+ }
+
+ return array_map(
+ static function ( array $provider_metadata ) {
+ unset( $provider_metadata['ai_client_classnames'] );
+ return \WordPress\AiClient\Providers\DTO\ProviderMetadata::fromArray( $provider_metadata );
+ },
+ $wp_ai_client_providers_metadata
+ );
+ }
+
+ /**
+ * Returns the metadata for all registered cloud providers across all instances of the PHP AI Client SDK.
+ *
+ * @since 7.0.0
+ *
+ * @return array Array of cloud provider metadata objects,
+ * keyed by provider ID.
+ */
+ public function get_all_cloud_providers_metadata() {
+ $all_providers = $this->get_all_providers_metadata();
+
+ return array_filter(
+ $all_providers,
+ static function ( $metadata ) {
+ return $metadata->getType()->isCloud();
+ }
+ );
+ }
+
+ /**
+ * Registers the settings for storing the API credentials.
+ *
+ * The setting will only be registered once, even if called multiple times.
+ *
+ * @since 7.0.0
+ */
+ public function register_settings() {
+ // Avoid registering the setting multiple times.
+ $registered_settings = get_registered_settings();
+ if ( isset( $registered_settings[ self::OPTION_PROVIDER_CREDENTIALS ] ) ) {
+ return;
+ }
+
+ register_setting(
+ self::OPTION_GROUP,
+ self::OPTION_PROVIDER_CREDENTIALS,
+ array(
+ 'type' => 'object',
+ 'default' => array(),
+ 'sanitize_callback' => array( $this, 'sanitize_credentials' ),
+ )
+ );
+ }
+
+ /**
+ * Sanitizes the provider credentials before saving.
+ *
+ * Filters out unknown providers and sanitizes each API key value.
+ *
+ * @since 7.0.0
+ *
+ * @param mixed $credentials The raw credentials input.
+ * @return array Sanitized credentials array.
+ */
+ public function sanitize_credentials( $credentials ) {
+ if ( ! is_array( $credentials ) ) {
+ return array();
+ }
+
+ // Assume that all cloud providers require an API key.
+ $providers_metadata_keyed_by_ids = $this->get_all_cloud_providers_metadata();
+
+ $credentials = array_intersect_key( $credentials, $providers_metadata_keyed_by_ids );
+ foreach ( $credentials as $provider_id => $api_key ) {
+ if ( ! is_string( $api_key ) ) {
+ unset( $credentials[ $provider_id ] );
+ continue;
+ }
+ $credentials[ $provider_id ] = sanitize_text_field( $api_key );
+ }
+ return $credentials;
+ }
+
+ /**
+ * Passes the stored API credentials to the PHP AI Client SDK.
+ *
+ * This method should be called on every request, before any API requests
+ * are made via the PHP AI Client SDK.
+ *
+ * @since 7.0.0
+ */
+ public function pass_credentials_to_client() {
+ $credentials = get_option( self::OPTION_PROVIDER_CREDENTIALS, array() );
+ if ( ! is_array( $credentials ) ) {
+ _doing_it_wrong(
+ __METHOD__,
+ __( 'Invalid format for stored provider credentials option.' ),
+ '7.0.0'
+ );
+ return;
+ }
+
+ $registry = AiClient::defaultRegistry();
+
+ foreach ( $credentials as $provider_id => $api_key ) {
+ if ( ! is_string( $api_key ) || '' === $api_key ) {
+ continue;
+ }
+
+ if ( ! $registry->hasProvider( $provider_id ) ) {
+ continue;
+ }
+
+ $registry->setProviderRequestAuthentication(
+ $provider_id,
+ new ApiKeyRequestAuthentication( $api_key )
+ );
+ }
+ }
+}
diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php
index 301b846343ee2..45d1dd7e22c5f 100644
--- a/src/wp-includes/default-filters.php
+++ b/src/wp-includes/default-filters.php
@@ -531,6 +531,7 @@
add_action( 'init', 'rest_api_init' );
add_action( 'rest_api_init', 'rest_api_default_filters', 10, 1 );
add_action( 'rest_api_init', 'register_initial_settings', 10 );
+add_action( 'admin_init', 'register_initial_settings' );
add_action( 'rest_api_init', 'create_initial_rest_routes', 99 );
add_action( 'parse_request', 'rest_api_loaded' );
diff --git a/src/wp-includes/option.php b/src/wp-includes/option.php
index 970ac39652195..23741b9d32d71 100644
--- a/src/wp-includes/option.php
+++ b/src/wp-includes/option.php
@@ -2969,6 +2969,8 @@ function register_initial_settings() {
'description' => __( 'Allow people to submit comments on new posts.' ),
)
);
+
+ $GLOBALS['wp_ai_client_credentials_manager']->register_settings();
}
/**
diff --git a/src/wp-settings.php b/src/wp-settings.php
index 90741401e800c..bf102b45bff21 100644
--- a/src/wp-settings.php
+++ b/src/wp-settings.php
@@ -477,6 +477,8 @@
WP_AI_Client_Discovery_Strategy::init();
WordPress\AiClient\AiClient::setCache( new WP_AI_Client_Cache() );
WordPress\AiClient\AiClient::setEventDispatcher( new WP_AI_Client_Event_Dispatcher() );
+require ABSPATH . WPINC . '/ai-client/adapters/class-wp-ai-client-credentials-manager.php';
+$GLOBALS['wp_ai_client_credentials_manager'] = new WP_AI_Client_Credentials_Manager();
// Load multisite-specific files.
if ( is_multisite() ) {
@@ -772,6 +774,10 @@
*/
do_action( 'init' );
+// WP AI Client - Collect providers and pass credentials after plugins have loaded.
+$GLOBALS['wp_ai_client_credentials_manager']->collect_providers();
+$GLOBALS['wp_ai_client_credentials_manager']->pass_credentials_to_client();
+
// Check site status.
if ( is_multisite() ) {
$file = ms_site_check();
diff --git a/tests/phpunit/tests/ai-client/wpAiClientCredentialsManager.php b/tests/phpunit/tests/ai-client/wpAiClientCredentialsManager.php
new file mode 100644
index 0000000000000..7dc00245b0702
--- /dev/null
+++ b/tests/phpunit/tests/ai-client/wpAiClientCredentialsManager.php
@@ -0,0 +1,372 @@
+saved_providers_metadata = $wp_ai_client_providers_metadata;
+ }
+
+ /**
+ * Restores state after each test.
+ */
+ public function tear_down() {
+ global $wp_ai_client_providers_metadata;
+ $wp_ai_client_providers_metadata = $this->saved_providers_metadata;
+ delete_option( WP_AI_Client_Credentials_Manager::OPTION_PROVIDER_CREDENTIALS );
+ parent::tear_down();
+ }
+
+ /**
+ * Test that collect_providers initializes the global as an array.
+ *
+ * @ticket TBD
+ */
+ public function test_collect_providers_initializes_global() {
+ global $wp_ai_client_providers_metadata;
+ $wp_ai_client_providers_metadata = null;
+
+ $manager = new WP_AI_Client_Credentials_Manager();
+ $manager->collect_providers();
+
+ $this->assertIsArray( $wp_ai_client_providers_metadata );
+
+ // Each entry should have the expected structure.
+ foreach ( $wp_ai_client_providers_metadata as $provider_id => $metadata ) {
+ $this->assertIsString( $provider_id );
+ $this->assertArrayHasKey( 'id', $metadata );
+ $this->assertArrayHasKey( 'name', $metadata );
+ $this->assertArrayHasKey( 'type', $metadata );
+ $this->assertArrayHasKey( 'ai_client_classnames', $metadata );
+ $this->assertIsArray( $metadata['ai_client_classnames'] );
+ }
+ }
+
+ /**
+ * Test that collect_providers does not duplicate entries when called multiple times.
+ *
+ * @ticket TBD
+ */
+ public function test_collect_providers_deduplicates() {
+ global $wp_ai_client_providers_metadata;
+ $wp_ai_client_providers_metadata = null;
+
+ $manager = new WP_AI_Client_Credentials_Manager();
+ $manager->collect_providers();
+
+ $first_count = count( $wp_ai_client_providers_metadata );
+
+ // Calling again should not duplicate providers.
+ $manager->collect_providers();
+ $this->assertCount( $first_count, $wp_ai_client_providers_metadata );
+ }
+
+ /**
+ * Test that collect_providers preserves existing entries in the global.
+ *
+ * @ticket TBD
+ */
+ public function test_collect_providers_preserves_existing_entries() {
+ global $wp_ai_client_providers_metadata;
+
+ // Seed the global with a fake provider entry not in the SDK registry.
+ $wp_ai_client_providers_metadata = array(
+ 'test-provider' => array(
+ 'id' => 'test-provider',
+ 'name' => 'Test Provider',
+ 'type' => 'cloud',
+ 'ai_client_classnames' => array( 'SomeOtherClient' => true ),
+ ),
+ );
+
+ $manager = new WP_AI_Client_Credentials_Manager();
+ $manager->collect_providers();
+
+ // The test-provider entry should still exist (not removed by collect_providers).
+ $this->assertArrayHasKey( 'test-provider', $wp_ai_client_providers_metadata );
+ $this->assertSame( 'Test Provider', $wp_ai_client_providers_metadata['test-provider']['name'] );
+ }
+
+ /**
+ * Test that get_all_providers_metadata returns ProviderMetadata objects.
+ *
+ * @ticket TBD
+ */
+ public function test_get_all_providers_metadata_returns_provider_metadata_objects() {
+ $manager = new WP_AI_Client_Credentials_Manager();
+ $providers = $manager->get_all_providers_metadata();
+
+ $this->assertIsArray( $providers );
+ foreach ( $providers as $metadata ) {
+ $this->assertInstanceOf( WordPress\AiClient\Providers\DTO\ProviderMetadata::class, $metadata );
+ }
+ }
+
+ /**
+ * Test that get_all_cloud_providers_metadata only returns cloud providers.
+ *
+ * @ticket TBD
+ */
+ public function test_get_all_cloud_providers_metadata_filters_to_cloud_only() {
+ $manager = new WP_AI_Client_Credentials_Manager();
+ $cloud_providers = $manager->get_all_cloud_providers_metadata();
+
+ $this->assertIsArray( $cloud_providers );
+ foreach ( $cloud_providers as $metadata ) {
+ $this->assertTrue(
+ $metadata->getType()->isCloud(),
+ sprintf( 'Provider "%s" should be a cloud provider.', $metadata->getId() )
+ );
+ }
+ }
+
+ /**
+ * Test that register_settings creates the setting.
+ *
+ * @ticket TBD
+ */
+ public function test_register_settings_creates_setting() {
+ $manager = new WP_AI_Client_Credentials_Manager();
+ $manager->register_settings();
+
+ $registered = get_registered_settings();
+ $this->assertArrayHasKey(
+ WP_AI_Client_Credentials_Manager::OPTION_PROVIDER_CREDENTIALS,
+ $registered
+ );
+
+ // Clean up.
+ unregister_setting( 'ai', WP_AI_Client_Credentials_Manager::OPTION_PROVIDER_CREDENTIALS );
+ }
+
+ /**
+ * Test that register_settings does not register twice.
+ *
+ * @ticket TBD
+ */
+ public function test_register_settings_idempotent() {
+ $manager = new WP_AI_Client_Credentials_Manager();
+ $manager->register_settings();
+ $manager->register_settings();
+
+ $registered = get_registered_settings();
+ $this->assertArrayHasKey(
+ WP_AI_Client_Credentials_Manager::OPTION_PROVIDER_CREDENTIALS,
+ $registered
+ );
+
+ // Clean up.
+ unregister_setting( 'ai', WP_AI_Client_Credentials_Manager::OPTION_PROVIDER_CREDENTIALS );
+ }
+
+ /**
+ * Test that sanitize_credentials filters out unknown providers.
+ *
+ * @ticket TBD
+ */
+ public function test_sanitize_credentials_filters_unknown_providers() {
+ global $wp_ai_client_providers_metadata;
+
+ // Seed a cloud provider in the global.
+ $wp_ai_client_providers_metadata = array(
+ 'test-cloud' => array(
+ 'id' => 'test-cloud',
+ 'name' => 'Test Cloud',
+ 'type' => 'cloud',
+ 'ai_client_classnames' => array( 'TestClient' => true ),
+ ),
+ );
+
+ $manager = new WP_AI_Client_Credentials_Manager();
+
+ $input = array(
+ 'test-cloud' => 'sk-valid-key',
+ 'nonexistent_provider' => 'sk-invalid-key',
+ );
+
+ $result = $manager->sanitize_credentials( $input );
+
+ $this->assertArrayHasKey( 'test-cloud', $result );
+ $this->assertArrayNotHasKey( 'nonexistent_provider', $result );
+ }
+
+ /**
+ * Test that sanitize_credentials applies sanitize_text_field.
+ *
+ * @ticket TBD
+ */
+ public function test_sanitize_credentials_sanitizes_values() {
+ global $wp_ai_client_providers_metadata;
+
+ $wp_ai_client_providers_metadata = array(
+ 'test-cloud' => array(
+ 'id' => 'test-cloud',
+ 'name' => 'Test Cloud',
+ 'type' => 'cloud',
+ 'ai_client_classnames' => array( 'TestClient' => true ),
+ ),
+ );
+
+ $manager = new WP_AI_Client_Credentials_Manager();
+
+ $input = array(
+ 'test-cloud' => " sk-key-with-whitespace\t",
+ );
+
+ $result = $manager->sanitize_credentials( $input );
+
+ $this->assertSame( 'sk-key-with-whitespace', $result['test-cloud'] );
+ }
+
+ /**
+ * Test that sanitize_credentials returns empty array for non-array input.
+ *
+ * @ticket TBD
+ */
+ public function test_sanitize_credentials_returns_empty_for_non_array() {
+ $manager = new WP_AI_Client_Credentials_Manager();
+
+ $this->assertSame( array(), $manager->sanitize_credentials( 'not-an-array' ) );
+ $this->assertSame( array(), $manager->sanitize_credentials( null ) );
+ }
+
+ /**
+ * Test that sanitize_credentials removes non-string values.
+ *
+ * @ticket TBD
+ */
+ public function test_sanitize_credentials_removes_non_string_values() {
+ global $wp_ai_client_providers_metadata;
+
+ $wp_ai_client_providers_metadata = array(
+ 'test-cloud' => array(
+ 'id' => 'test-cloud',
+ 'name' => 'Test Cloud',
+ 'type' => 'cloud',
+ 'ai_client_classnames' => array( 'TestClient' => true ),
+ ),
+ );
+
+ $manager = new WP_AI_Client_Credentials_Manager();
+
+ $input = array(
+ 'test-cloud' => array( 'not', 'a', 'string' ),
+ );
+
+ $result = $manager->sanitize_credentials( $input );
+
+ $this->assertArrayNotHasKey( 'test-cloud', $result );
+ }
+
+ /**
+ * Test that sanitize_credentials filters out non-cloud providers.
+ *
+ * @ticket TBD
+ */
+ public function test_sanitize_credentials_filters_non_cloud_providers() {
+ global $wp_ai_client_providers_metadata;
+
+ $wp_ai_client_providers_metadata = array(
+ 'test-cloud' => array(
+ 'id' => 'test-cloud',
+ 'name' => 'Test Cloud',
+ 'type' => 'cloud',
+ 'ai_client_classnames' => array( 'TestClient' => true ),
+ ),
+ 'test-server' => array(
+ 'id' => 'test-server',
+ 'name' => 'Test Server',
+ 'type' => 'server',
+ 'ai_client_classnames' => array( 'TestClient' => true ),
+ ),
+ );
+
+ $manager = new WP_AI_Client_Credentials_Manager();
+
+ $input = array(
+ 'test-cloud' => 'sk-cloud-key',
+ 'test-server' => 'sk-server-key',
+ );
+
+ $result = $manager->sanitize_credentials( $input );
+
+ $this->assertArrayHasKey( 'test-cloud', $result );
+ $this->assertArrayNotHasKey( 'test-server', $result );
+ }
+
+ /**
+ * Test that pass_credentials_to_client skips providers not in the registry.
+ *
+ * @ticket TBD
+ */
+ public function test_pass_credentials_to_client_skips_unregistered_providers() {
+ $manager = new WP_AI_Client_Credentials_Manager();
+
+ // Set credentials for a provider that doesn't exist in the SDK registry.
+ update_option(
+ WP_AI_Client_Credentials_Manager::OPTION_PROVIDER_CREDENTIALS,
+ array( 'nonexistent-provider' => 'sk-test-key' )
+ );
+
+ // This should not throw any errors.
+ $manager->pass_credentials_to_client();
+
+ // Verify by checking the registry doesn't have the provider.
+ $registry = WordPress\AiClient\AiClient::defaultRegistry();
+ $this->assertFalse( $registry->hasProvider( 'nonexistent-provider' ) );
+ }
+
+ /**
+ * Test that pass_credentials_to_client handles invalid option value gracefully.
+ *
+ * @ticket TBD
+ */
+ public function test_pass_credentials_to_client_handles_invalid_option() {
+ $manager = new WP_AI_Client_Credentials_Manager();
+
+ // Set a non-array value for the option.
+ update_option(
+ WP_AI_Client_Credentials_Manager::OPTION_PROVIDER_CREDENTIALS,
+ 'not-an-array'
+ );
+
+ // This should trigger _doing_it_wrong but not fatal.
+ $this->setExpectedIncorrectUsage( 'WP_AI_Client_Credentials_Manager::pass_credentials_to_client' );
+ $manager->pass_credentials_to_client();
+ }
+
+ /**
+ * Test that pass_credentials_to_client skips empty API keys.
+ *
+ * @ticket TBD
+ */
+ public function test_pass_credentials_to_client_skips_empty_keys() {
+ $manager = new WP_AI_Client_Credentials_Manager();
+
+ // Set credentials with empty values for a non-existent provider.
+ update_option(
+ WP_AI_Client_Credentials_Manager::OPTION_PROVIDER_CREDENTIALS,
+ array( 'some-provider' => '' )
+ );
+
+ // Should not throw any errors - empty keys are silently skipped.
+ $manager->pass_credentials_to_client();
+ $this->assertTrue( true );
+ }
+}