Skip to content
Open
4 changes: 3 additions & 1 deletion src/wp-includes/ai-client.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -55,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 );
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public function clear(): bool {
* @param mixed $default_value Default value to return for keys that do not exist.
* @return array<string, mixed> A list of key => value pairs.
*/
public function getMultiple( $keys, $default_value = null ) {
public function getMultiple( $keys, $default_value = null ): array {
/**
* Keys array.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
22 changes: 20 additions & 2 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 @@ -190,14 +190,32 @@ public function __construct( ProviderRegistry $registry, $prompt = null ) {
$this->error = $this->exception_to_wp_error( $e );
}

$default_timeout = 30.0;

/**
* 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|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.
*/
$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 ) && (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.' ),
'<code>wp_ai_client_default_request_timeout</code>'
),
'7.0.0'
);
}
Comment on lines +203 to +218
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if there is a reason to support a separate null , I'd still coerce to avoid the confusing $default_timeout -> $timeout -> $default_timeout flow. But I don't think it's necessary.

We definitely don't need the extra handholding for < 0 validation, since that's handled by setTimeout directly.

Suggested change
$timeout = apply_filters( 'wp_ai_client_default_request_timeout', $default_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.' ),
'<code>wp_ai_client_default_request_timeout</code>'
),
'7.0.0'
);
}
$default_timeout = (float) apply_filters( 'wp_ai_client_default_request_timeout', 30.0 );
// Coerce falsy values to null. But IMO even this is unnecessary and `0.0` is fine.
$default_timeout = 0.0 === $default_timeout ? null : $default_timeout;


$this->builder->usingRequestOptions(
RequestOptions::fromArray(
Expand Down
66 changes: 62 additions & 4 deletions tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -192,15 +192,16 @@ 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
* @ticket 65094
*/
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 () {
return 45;
return 45.5;
}
);

Expand All @@ -210,7 +211,64 @@ 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() );
}

/**
* 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 an invalid value.
*
* @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_invalid_request_timeout( $timeout ) {
add_filter(
'wp_ai_client_default_request_timeout',
static function () use ( $timeout ) {
return $timeout;
}
);

$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() );
}

/**
* Data provider for test_constructor_disallows_overriding_with_invalid_request_timeout().
*
* @return array<string, array{0: mixed}>
*/
public function data_invalid_request_timeouts(): array {
return array(
'negative number' => array( -1 ),
'array' => array( array() ),
);
}

/**
Expand Down
Loading