From 438011b50e05f139497f1acbed8ff02e004b9d25 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 16 Apr 2026 15:11:53 -0700 Subject: [PATCH 1/9] Add missing PHP imports --- src/wp-includes/ai-client.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wp-includes/ai-client.php b/src/wp-includes/ai-client.php index 4fc20166fb8bb..d83acac7ab01f 100644 --- a/src/wp-includes/ai-client.php +++ b/src/wp-includes/ai-client.php @@ -8,6 +8,8 @@ */ use WordPress\AiClient\AiClient; +use WordPress\AiClient\Messages\DTO\Message; +use WordPress\AiClient\Messages\DTO\MessagePart; /** * Returns whether AI features are supported in the current environment. From 7eb2c19a1e25fb2c15b90e64115b97230b6aa1a8 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 16 Apr 2026 15:12:07 -0700 Subject: [PATCH 2/9] Add missing return types --- src/wp-includes/ai-client.php | 2 +- src/wp-includes/ai-client/adapters/class-wp-ai-client-cache.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/ai-client.php b/src/wp-includes/ai-client.php index d83acac7ab01f..b38c7b721416d 100644 --- a/src/wp-includes/ai-client.php +++ b/src/wp-includes/ai-client.php @@ -57,6 +57,6 @@ function wp_supports_ai(): bool { * conversations. Default null. * @return WP_AI_Client_Prompt_Builder The prompt builder instance. */ -function wp_ai_client_prompt( $prompt = null ) { +function wp_ai_client_prompt( $prompt = null ): WP_AI_Client_Prompt_Builder { return new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry(), $prompt ); } diff --git a/src/wp-includes/ai-client/adapters/class-wp-ai-client-cache.php b/src/wp-includes/ai-client/adapters/class-wp-ai-client-cache.php index 18d85eee6c9e6..45504897485f7 100644 --- a/src/wp-includes/ai-client/adapters/class-wp-ai-client-cache.php +++ b/src/wp-includes/ai-client/adapters/class-wp-ai-client-cache.php @@ -104,7 +104,7 @@ public function clear(): bool { * @param mixed $default_value Default value to return for keys that do not exist. * @return array A list of key => value pairs. */ - public function getMultiple( $keys, $default_value = null ) { + public function getMultiple( $keys, $default_value = null ): array { /** * Keys array. * From 8006183b00f1aa1e257ffde45b38afc0e476e616 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 16 Apr 2026 15:12:27 -0700 Subject: [PATCH 3/9] Use PHP class member variable type hints --- .../ai-client/adapters/class-wp-ai-client-http-client.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/ai-client/adapters/class-wp-ai-client-http-client.php b/src/wp-includes/ai-client/adapters/class-wp-ai-client-http-client.php index f1827db0e437c..f6c6dea441d1c 100644 --- a/src/wp-includes/ai-client/adapters/class-wp-ai-client-http-client.php +++ b/src/wp-includes/ai-client/adapters/class-wp-ai-client-http-client.php @@ -32,17 +32,15 @@ class WP_AI_Client_HTTP_Client implements ClientInterface, ClientWithOptionsInte * Response factory instance. * * @since 7.0.0 - * @var ResponseFactoryInterface */ - private $response_factory; + private ResponseFactoryInterface $response_factory; /** * Stream factory instance. * * @since 7.0.0 - * @var StreamFactoryInterface */ - private $stream_factory; + private StreamFactoryInterface $stream_factory; /** * Constructor. From 63542e415e9b34ab02e76ec11d26b1212553299d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 16 Apr 2026 15:13:11 -0700 Subject: [PATCH 4/9] Ensure default timeout is number casted to float and is at least zero --- .../ai-client/class-wp-ai-client-prompt-builder.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php b/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php index d1f2271bd47d3..72d706fca95f9 100644 --- a/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php +++ b/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php @@ -190,14 +190,22 @@ public function __construct( ProviderRegistry $registry, $prompt = null ) { $this->error = $this->exception_to_wp_error( $e ); } + $default_timeout = 30; + /** * Filters the default request timeout in seconds for AI Client HTTP requests. * * @since 7.0.0 * - * @param int $default_timeout The default timeout in seconds. + * @param float $default_timeout The default timeout in seconds. */ - $default_timeout = (int) apply_filters( 'wp_ai_client_default_request_timeout', 30 ); + $timeout = apply_filters( 'wp_ai_client_default_request_timeout', $default_timeout ); + if ( is_numeric( $timeout ) ) { + $timeout = (float) $timeout; + if ( $timeout >= 0 ) { + $default_timeout = $timeout; + } + } $this->builder->usingRequestOptions( RequestOptions::fromArray( From ed8d8d6a207a7f4db93c1382fc8ce8062819b813 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 17 Apr 2026 19:47:17 -0700 Subject: [PATCH 5/9] Use float value explicitly for timeout --- .../ai-client/class-wp-ai-client-prompt-builder.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php b/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php index 72d706fca95f9..84ce67b2cf33d 100644 --- a/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php +++ b/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php @@ -190,19 +190,19 @@ public function __construct( ProviderRegistry $registry, $prompt = null ) { $this->error = $this->exception_to_wp_error( $e ); } - $default_timeout = 30; + $default_timeout = 30.0; /** * Filters the default request timeout in seconds for AI Client HTTP requests. * * @since 7.0.0 * - * @param float $default_timeout The default timeout in seconds. + * @param float $default_timeout The default timeout in seconds. Must be greater than or equal to zero. */ $timeout = apply_filters( 'wp_ai_client_default_request_timeout', $default_timeout ); if ( is_numeric( $timeout ) ) { $timeout = (float) $timeout; - if ( $timeout >= 0 ) { + if ( $timeout >= 0.0 ) { $default_timeout = $timeout; } } From 9925c25b3d4973b3b2c4efdf1929c8c23ce85628 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 17 Apr 2026 20:19:43 -0700 Subject: [PATCH 6/9] Allow filtered timeout to be null and warn when invalid value provided --- .../class-wp-ai-client-prompt-builder.php | 22 +++++-- .../ai-client/wpAiClientPromptBuilder.php | 64 ++++++++++++++++++- 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php b/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php index 84ce67b2cf33d..09c59142ae5e8 100644 --- a/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php +++ b/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php @@ -197,14 +197,24 @@ public function __construct( ProviderRegistry $registry, $prompt = null ) { * * @since 7.0.0 * - * @param float $default_timeout The default timeout in seconds. Must be greater than or equal to zero. + * @param float|null $default_timeout The default timeout in seconds, or null to disable the timeout. + * If not null, must be greater than or equal to zero. */ $timeout = apply_filters( 'wp_ai_client_default_request_timeout', $default_timeout ); - if ( is_numeric( $timeout ) ) { - $timeout = (float) $timeout; - if ( $timeout >= 0.0 ) { - $default_timeout = $timeout; - } + if ( is_numeric( $timeout ) && (float) $timeout >= 0.0 ) { + $default_timeout = (float) $timeout; + } elseif ( null === $timeout ) { + $default_timeout = null; + } else { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: %s: wp_ai_client_default_request_timeout */ + __( 'The %s filter must return a non-negative number or null.' ), + 'wp_ai_client_default_request_timeout' + ), + '7.0.0' + ); } $this->builder->usingRequestOptions( diff --git a/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php b/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php index 3630b0bab403a..3b6261a06191c 100644 --- a/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php +++ b/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php @@ -192,11 +192,11 @@ public function test_constructor_sets_default_request_timeout() { } /** - * Test that the constructor allows overriding the default request timeout. + * Test that the constructor allows overriding the default request timeout with a higher value. * * @ticket 64591 */ - public function test_constructor_allows_overriding_request_timeout() { + public function test_constructor_allows_overriding_request_timeout_with_higher_value() { add_filter( 'wp_ai_client_default_request_timeout', static function () { @@ -213,6 +213,66 @@ static function () { $this->assertEquals( 45, $request_options->getTimeout() ); } + /** + * Test that the constructor allows overriding the default request timeout with null. + * + * @ticket 65094 + */ + public function test_constructor_allows_overriding_request_timeout_with_null() { + add_filter( 'wp_ai_client_default_request_timeout', '__return_null' ); + + $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() ); + + /** @var RequestOptions $request_options */ + $request_options = $this->get_wrapped_prompt_builder_property_value( $builder, 'requestOptions' ); + + $this->assertInstanceOf( RequestOptions::class, $request_options ); + $this->assertNull( $request_options->getTimeout() ); + } + + /** + * Test that the constructor disallows overriding the default request timeout with a invalid value. + * + * @ticket 65094 + * + * @expectedIncorrectUsage WP_AI_Client_Prompt_Builder::__construct + */ + public function test_constructor_disallows_overriding_with_negative_request_timeout() { + add_filter( + 'wp_ai_client_default_request_timeout', + static function () { + return -1; + } + ); + + $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() ); + + /** @var RequestOptions $request_options */ + $request_options = $this->get_wrapped_prompt_builder_property_value( $builder, 'requestOptions' ); + + $this->assertInstanceOf( RequestOptions::class, $request_options ); + $this->assertEquals( 30, $request_options->getTimeout() ); + } + + /** + * Test that the constructor disallows overriding the default request timeout with a invalid value. + * + * @ticket 65094 + * + * @expectedIncorrectUsage WP_AI_Client_Prompt_Builder::__construct + */ + public function test_constructor_disallows_overriding_with_bad_request_timeout_type() { + add_filter( 'wp_ai_client_default_request_timeout', '__return_empty_array' ); + + $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() ); + + /** @var RequestOptions $request_options */ + $request_options = $this->get_wrapped_prompt_builder_property_value( $builder, 'requestOptions' ); + + $this->assertInstanceOf( RequestOptions::class, $request_options ); + $this->assertEquals( 30, $request_options->getTimeout() ); + } + /** * Test method chaining with fluent methods. * From f01e50d2062aa0cca858d3ace075a06b8e1ea342 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 17 Apr 2026 21:01:13 -0700 Subject: [PATCH 7/9] Update test to use float value --- tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php b/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php index 3b6261a06191c..607ab20e9cdf3 100644 --- a/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php +++ b/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php @@ -195,12 +195,13 @@ public function test_constructor_sets_default_request_timeout() { * Test that the constructor allows overriding the default request timeout with a higher value. * * @ticket 64591 + * @ticket 65094 */ public function test_constructor_allows_overriding_request_timeout_with_higher_value() { add_filter( 'wp_ai_client_default_request_timeout', static function () { - return 45; + return 45.5; } ); @@ -210,7 +211,7 @@ static function () { $request_options = $this->get_wrapped_prompt_builder_property_value( $builder, 'requestOptions' ); $this->assertInstanceOf( RequestOptions::class, $request_options ); - $this->assertEquals( 45, $request_options->getTimeout() ); + $this->assertEquals( 45.5, $request_options->getTimeout() ); } /** From 00684221b8e4e66b03d291be72fb6a8994d5a476 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 20 Apr 2026 11:51:41 -0700 Subject: [PATCH 8/9] Fix grammar typo --- tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php b/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php index 607ab20e9cdf3..9d78bce705adf 100644 --- a/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php +++ b/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php @@ -232,7 +232,7 @@ public function test_constructor_allows_overriding_request_timeout_with_null() { } /** - * Test that the constructor disallows overriding the default request timeout with a invalid value. + * Test that the constructor disallows overriding the default request timeout with an invalid value. * * @ticket 65094 * @@ -256,7 +256,7 @@ static function () { } /** - * Test that the constructor disallows overriding the default request timeout with a invalid value. + * Test that the constructor disallows overriding the default request timeout with an invalid value. * * @ticket 65094 * From 8e4c3dfc1d1e127264bf9b63bc5a3e81e4c8dd09 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 20 Apr 2026 11:59:37 -0700 Subject: [PATCH 9/9] Use a data provider for invalid request timeout tests Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ai-client/wpAiClientPromptBuilder.php | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php b/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php index 9d78bce705adf..ab28bfc760c83 100644 --- a/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php +++ b/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php @@ -236,13 +236,17 @@ public function test_constructor_allows_overriding_request_timeout_with_null() { * * @ticket 65094 * + * @dataProvider data_invalid_request_timeouts + * * @expectedIncorrectUsage WP_AI_Client_Prompt_Builder::__construct + * + * @param mixed $timeout The invalid timeout value returned by the filter. */ - public function test_constructor_disallows_overriding_with_negative_request_timeout() { + public function test_constructor_disallows_overriding_with_invalid_request_timeout( $timeout ) { add_filter( 'wp_ai_client_default_request_timeout', - static function () { - return -1; + static function () use ( $timeout ) { + return $timeout; } ); @@ -256,22 +260,15 @@ static function () { } /** - * Test that the constructor disallows overriding the default request timeout with an invalid value. + * Data provider for test_constructor_disallows_overriding_with_invalid_request_timeout(). * - * @ticket 65094 - * - * @expectedIncorrectUsage WP_AI_Client_Prompt_Builder::__construct + * @return array */ - public function test_constructor_disallows_overriding_with_bad_request_timeout_type() { - add_filter( 'wp_ai_client_default_request_timeout', '__return_empty_array' ); - - $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() ); - - /** @var RequestOptions $request_options */ - $request_options = $this->get_wrapped_prompt_builder_property_value( $builder, 'requestOptions' ); - - $this->assertInstanceOf( RequestOptions::class, $request_options ); - $this->assertEquals( 30, $request_options->getTimeout() ); + public function data_invalid_request_timeouts(): array { + return array( + 'negative number' => array( -1 ), + 'array' => array( array() ), + ); } /**