diff --git a/inc/Abilities/SettingsAbilities.php b/inc/Abilities/SettingsAbilities.php
index a68456b0f..f01346eef 100644
--- a/inc/Abilities/SettingsAbilities.php
+++ b/inc/Abilities/SettingsAbilities.php
@@ -110,10 +110,14 @@ private function registerUpdateSettings(): void {
'description' => 'Per-mode provider/model overrides keyed by mode id',
),
'max_turns' => array( 'type' => 'integer' ),
- 'wp_ai_client_connect_timeout' => array(
+ 'wp_ai_client_connect_timeout' => array(
'type' => 'number',
'description' => 'Connection timeout in seconds for wp-ai-client provider requests.',
),
+ 'wp_ai_client_request_timeout' => array(
+ 'type' => 'number',
+ 'description' => 'Full request timeout in seconds for wp-ai-client provider requests.',
+ ),
'disabled_tools' => array( 'type' => 'object' ),
'ai_provider_keys' => array( 'type' => 'object' ),
'queue_tuning' => array(
@@ -357,7 +361,8 @@ public function executeGetSettings( array $input ): array {
'default_model' => $settings['default_model'] ?? '',
'mode_models' => $settings['mode_models'] ?? array(),
'max_turns' => $settings['max_turns'] ?? $defaults['max_turns'],
- 'wp_ai_client_connect_timeout' => $settings['wp_ai_client_connect_timeout'] ?? $defaults['wp_ai_client_connect_timeout'],
+ 'wp_ai_client_connect_timeout' => $settings['wp_ai_client_connect_timeout'] ?? $defaults['wp_ai_client_connect_timeout'],
+ 'wp_ai_client_request_timeout' => $settings['wp_ai_client_request_timeout'] ?? $defaults['wp_ai_client_request_timeout'],
'disabled_tools' => $settings['disabled_tools'] ?? array(),
'ai_provider_keys' => $masked_keys,
'queue_tuning' => wp_parse_args( $settings['queue_tuning'] ?? array(), $defaults['queue_tuning'] ),
@@ -463,8 +468,19 @@ public function executeUpdateSettings( array $input ): array {
}
if ( isset( $input['wp_ai_client_connect_timeout'] ) && is_numeric( $input['wp_ai_client_connect_timeout'] ) ) {
- $all_settings['wp_ai_client_connect_timeout'] = max( 0.0, min( 300.0, (float) $input['wp_ai_client_connect_timeout'] ) );
- $handled_keys[] = 'wp_ai_client_connect_timeout';
+ $all_settings['wp_ai_client_connect_timeout'] = max(
+ 0.0,
+ min( PluginSettings::MAX_WP_AI_CLIENT_CONNECT_TIMEOUT, (float) $input['wp_ai_client_connect_timeout'] )
+ );
+ $handled_keys[] = 'wp_ai_client_connect_timeout';
+ }
+
+ if ( isset( $input['wp_ai_client_request_timeout'] ) && is_numeric( $input['wp_ai_client_request_timeout'] ) ) {
+ $all_settings['wp_ai_client_request_timeout'] = max(
+ 0.0,
+ min( PluginSettings::MAX_WP_AI_CLIENT_REQUEST_TIMEOUT, (float) $input['wp_ai_client_request_timeout'] )
+ );
+ $handled_keys[] = 'wp_ai_client_request_timeout';
}
if ( isset( $input['disabled_tools'] ) ) {
diff --git a/inc/Api/Settings.php b/inc/Api/Settings.php
index b40db2457..9cd3f2286 100644
--- a/inc/Api/Settings.php
+++ b/inc/Api/Settings.php
@@ -323,6 +323,7 @@ public static function handle_get_settings( $request ) {
'success' => true,
'data' => array(
'settings' => $result['settings'],
+ 'defaults' => $result['defaults'] ?? array(),
'global_tools' => $result['global_tools'],
),
)
diff --git a/inc/Core/Admin/Settings/assets/react/components/tabs/GeneralTab.jsx b/inc/Core/Admin/Settings/assets/react/components/tabs/GeneralTab.jsx
index b9ed293fa..3266fd6a8 100644
--- a/inc/Core/Admin/Settings/assets/react/components/tabs/GeneralTab.jsx
+++ b/inc/Core/Admin/Settings/assets/react/components/tabs/GeneralTab.jsx
@@ -11,7 +11,7 @@
import { useEffect } from '@wordpress/element';
/**
- * Internal dependencies
+ * External dependencies
*/
import { useSettings, useUpdateSettings } from '@shared/queries/settings';
import { useFormState } from '@shared/hooks/useFormState';
@@ -24,6 +24,8 @@ const EMPTY_FORM = {
file_retention_days: 7,
chat_retention_days: 90,
chat_ai_titles_enabled: true,
+ wp_ai_client_connect_timeout: 15,
+ wp_ai_client_request_timeout: 300,
flows_per_page: 20,
jobs_per_page: 50,
queue_tuning: {
@@ -65,6 +67,10 @@ const GeneralTab = () => {
chunk_size: 10,
chunk_delay: 30,
};
+ const transportDefaults = {
+ connectTimeout: data?.defaults?.wp_ai_client_connect_timeout ?? 15,
+ requestTimeout: data?.defaults?.wp_ai_client_request_timeout ?? 300,
+ };
const form = useFormState( {
initialData: EMPTY_FORM,
@@ -87,6 +93,10 @@ const GeneralTab = () => {
data.settings.chat_retention_days ?? EMPTY_FORM.chat_retention_days,
chat_ai_titles_enabled:
data.settings.chat_ai_titles_enabled ?? EMPTY_FORM.chat_ai_titles_enabled,
+ wp_ai_client_connect_timeout:
+ data.settings.wp_ai_client_connect_timeout ?? transportDefaults.connectTimeout,
+ wp_ai_client_request_timeout:
+ data.settings.wp_ai_client_request_timeout ?? transportDefaults.requestTimeout,
flows_per_page:
data.settings.flows_per_page ?? EMPTY_FORM.flows_per_page,
jobs_per_page:
@@ -173,6 +183,70 @@ const GeneralTab = () => {
+
| File retention (days) |
diff --git a/inc/Core/PluginSettings.php b/inc/Core/PluginSettings.php
index a24d90d46..457b6ade3 100644
--- a/inc/Core/PluginSettings.php
+++ b/inc/Core/PluginSettings.php
@@ -20,8 +20,11 @@
class PluginSettings {
- public const DEFAULT_MAX_TURNS = 25;
- public const DEFAULT_WP_AI_CLIENT_CONNECT_TIMEOUT = 30.0;
+ public const DEFAULT_MAX_TURNS = 25;
+ public const DEFAULT_WP_AI_CLIENT_CONNECT_TIMEOUT = 15.0;
+ public const DEFAULT_WP_AI_CLIENT_REQUEST_TIMEOUT = 300.0;
+ public const MAX_WP_AI_CLIENT_CONNECT_TIMEOUT = 300.0;
+ public const MAX_WP_AI_CLIENT_REQUEST_TIMEOUT = 900.0;
private static ?array $cache = null;
private static array $agent_model_cache = array();
@@ -55,12 +58,13 @@ public static function getDefaultQueueTuning(): array {
/**
* Get centralized plugin defaults used by backend and admin UI.
*
- * @return array{max_turns:int,wp_ai_client_connect_timeout:float,queue_tuning:array{concurrent_batches:int,batch_size:int,time_limit:int,chunk_size:int,chunk_delay:int}}
+ * @return array{max_turns:int,wp_ai_client_connect_timeout:float,wp_ai_client_request_timeout:float,queue_tuning:array{concurrent_batches:int,batch_size:int,time_limit:int,chunk_size:int,chunk_delay:int}}
*/
public static function getDefaults(): array {
return array(
'max_turns' => self::DEFAULT_MAX_TURNS,
'wp_ai_client_connect_timeout' => self::DEFAULT_WP_AI_CLIENT_CONNECT_TIMEOUT,
+ 'wp_ai_client_request_timeout' => self::DEFAULT_WP_AI_CLIENT_REQUEST_TIMEOUT,
'queue_tuning' => self::getDefaultQueueTuning(),
);
}
diff --git a/inc/Engine/AI/RequestBuilder.php b/inc/Engine/AI/RequestBuilder.php
index 4bd36585f..9cff53f49 100644
--- a/inc/Engine/AI/RequestBuilder.php
+++ b/inc/Engine/AI/RequestBuilder.php
@@ -21,16 +21,6 @@
class RequestBuilder {
- /**
- * Default timeout for Data Machine wp-ai-client requests.
- *
- * WordPress AI Client defaults to 30 seconds, which is too short for
- * non-streaming LLM requests with large prompts. Studio's cURL low-speed
- * watchdog is longer than this; without raising the AI Client request
- * timeout, the request-level cap wins first.
- */
- private const DEFAULT_WP_AI_CLIENT_REQUEST_TIMEOUT = 300.0;
-
/**
* Build standardized AI request for any execution mode.
*
@@ -63,10 +53,10 @@ public static function build(
) {
WpAiClientCache::install();
- $assembled = self::assemble( $messages, $provider, $model, $tools, $mode, $payload );
- $request = $assembled['request'];
- $provider_request = ProviderRequestAssembler::toProviderRequest( $request );
- $prompt_context = self::wpAiClientPromptContext( $request['messages'] ?? array() );
+ $assembled = self::assemble( $messages, $provider, $model, $tools, $mode, $payload );
+ $request = $assembled['request'];
+ $provider_request = ProviderRequestAssembler::toProviderRequest( $request );
+ $prompt_context = self::wpAiClientPromptContext( $request['messages'] ?? array() );
if ( '' !== $prompt_context['prompt'] ) {
$provider_request['prompt'] = $prompt_context['prompt'];
}
@@ -130,14 +120,16 @@ public static function build(
return new \WP_Error( 'wp_ai_client_unavailable', $unavailable_reason );
}
- $result = null;
- $request_options = null;
- $request_timeout = self::wpAiClientRequestTimeout( $mode, $provider, $model, $payload );
- $connect_timeout = self::wpAiClientConnectTimeout( $mode, $provider, $model, $payload, $request_timeout );
- $timeout_filter = static function ( $default_timeout ) use ( $request_timeout ) {
+ $result = null;
+ $request_options = null;
+ $transport_profile = self::wpAiClientTransportProfile( $mode, $provider, $model, $payload );
+ $request_timeout = (float) $transport_profile['request_timeout'];
+ $connect_timeout = (float) $transport_profile['connect_timeout'];
+ $request_metadata['transport'] = $transport_profile;
+ $timeout_filter = static function ( $default_timeout ) use ( $request_timeout ) {
return max( (float) $default_timeout, $request_timeout );
};
- $curl_filter = static function ( $handle ) use ( $request_timeout, $connect_timeout ) {
+ $curl_filter = static function ( $handle ) use ( $request_timeout, $connect_timeout ) {
if ( defined( 'CURLOPT_CONNECTTIMEOUT' ) ) {
curl_setopt( $handle, CURLOPT_CONNECTTIMEOUT, (int) ceil( $connect_timeout ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt -- WordPress exposes the cURL handle only through this hook.
}
@@ -154,6 +146,8 @@ public static function build(
try {
add_filter( 'wp_ai_client_default_request_timeout', $timeout_filter, 10, 1 );
add_action( 'http_api_curl', $curl_filter, 10, 1 );
+ $transport_profile['curl_hook_installed'] = true;
+ $request_metadata['transport'] = $transport_profile;
$registry = \WordPress\AiClient\AiClient::defaultRegistry();
/** @var callable $has_provider wp-ai-client exposes this through __call() in some versions. */
@@ -180,11 +174,23 @@ public static function build(
$request_options = new \WordPress\AiClient\Providers\Http\DTO\RequestOptions();
$request_options->setTimeout( $request_timeout );
$request_options->setConnectTimeout( $connect_timeout );
+ $transport_profile['request_options_used'] = true;
+ $request_metadata['transport'] = $transport_profile;
if ( is_object( $model_instance ) && method_exists( $model_instance, 'setRequestOptions' ) ) {
$model_instance->setRequestOptions( $request_options );
}
}
+ do_action(
+ 'datamachine_log',
+ 'debug',
+ 'AI transport profile resolved',
+ array_filter(
+ $transport_profile,
+ fn( $v ) => null !== $v
+ )
+ );
+
// wp-ai-client refuses to construct a MessagePart from an empty
// string. Only pass the prompt text when it is non-empty; otherwise
// fall back to instantiating the builder without a prompt and let
@@ -239,21 +245,23 @@ public static function build(
}
}
+ $dispatch_context = array_filter(
+ array_merge(
+ $transport_profile,
+ array(
+ 'success' => ! is_wp_error( $result ),
+ 'error_code' => is_wp_error( $result ) ? $result->get_error_code() : null,
+ 'error_message' => is_wp_error( $result ) ? $result->get_error_message() : null,
+ )
+ ),
+ fn( $v ) => null !== $v
+ );
+
do_action(
'datamachine_log',
- 'debug',
+ is_wp_error( $result ) ? 'error' : 'debug',
'AI request dispatched via wp-ai-client',
- array_filter(
- array(
- 'mode' => $mode,
- 'job_id' => $payload['job_id'] ?? null,
- 'flow_step_id' => $payload['flow_step_id'] ?? null,
- 'provider' => $provider,
- 'model' => $model,
- 'success' => ! is_wp_error( $result ),
- ),
- fn( $v ) => null !== $v
- )
+ $dispatch_context
);
return $result;
@@ -392,9 +400,21 @@ private static function wpAiClientMessageText( $content ): ?string {
* @return float Timeout in seconds.
*/
private static function wpAiClientRequestTimeout( string $mode, string $provider, string $model, array $payload ): float {
- $timeout = apply_filters(
+ $setting_default = PluginSettings::get(
+ 'wp_ai_client_request_timeout',
+ PluginSettings::DEFAULT_WP_AI_CLIENT_REQUEST_TIMEOUT
+ );
+ if ( ! is_numeric( $setting_default ) ) {
+ $setting_default = PluginSettings::DEFAULT_WP_AI_CLIENT_REQUEST_TIMEOUT;
+ }
+
+ $default_timeout = max(
+ 0.0,
+ min( PluginSettings::MAX_WP_AI_CLIENT_REQUEST_TIMEOUT, (float) $setting_default )
+ );
+ $timeout = apply_filters(
'datamachine_wp_ai_client_request_timeout',
- self::DEFAULT_WP_AI_CLIENT_REQUEST_TIMEOUT,
+ $default_timeout,
$mode,
$provider,
$model,
@@ -402,7 +422,7 @@ private static function wpAiClientRequestTimeout( string $mode, string $provider
);
if ( ! is_numeric( $timeout ) ) {
- return self::DEFAULT_WP_AI_CLIENT_REQUEST_TIMEOUT;
+ return $default_timeout;
}
return max( 0.0, (float) $timeout );
@@ -427,8 +447,14 @@ private static function wpAiClientConnectTimeout( string $mode, string $provider
$setting_default = PluginSettings::DEFAULT_WP_AI_CLIENT_CONNECT_TIMEOUT;
}
- $default_timeout = min( max( 0.0, (float) $setting_default ), $request_timeout );
- $timeout = apply_filters(
+ $default_timeout = min(
+ max(
+ 0.0,
+ min( PluginSettings::MAX_WP_AI_CLIENT_CONNECT_TIMEOUT, (float) $setting_default )
+ ),
+ $request_timeout
+ );
+ $timeout = apply_filters(
'datamachine_wp_ai_client_connect_timeout',
$default_timeout,
$mode,
@@ -445,6 +471,33 @@ private static function wpAiClientConnectTimeout( string $mode, string $provider
return max( 0.0, (float) $timeout );
}
+ /**
+ * Resolve the transport profile Data Machine applies to a wp-ai-client request.
+ *
+ * @param string $mode Execution mode.
+ * @param string $provider Provider identifier.
+ * @param string $model Model identifier.
+ * @param array $payload Step payload.
+ * @return array Resolved transport profile for logging and inspection.
+ */
+ public static function wpAiClientTransportProfile( string $mode, string $provider, string $model, array $payload ): array {
+ $request_timeout = self::wpAiClientRequestTimeout( $mode, $provider, $model, $payload );
+ $connect_timeout = self::wpAiClientConnectTimeout( $mode, $provider, $model, $payload, $request_timeout );
+
+ return array(
+ 'mode' => $mode,
+ 'provider' => $provider,
+ 'model' => $model,
+ 'job_id' => $payload['job_id'] ?? null,
+ 'flow_step_id' => $payload['flow_step_id'] ?? null,
+ 'request_timeout' => $request_timeout,
+ 'connect_timeout' => $connect_timeout,
+ 'request_options_class_available' => class_exists( '\\WordPress\\AiClient\\Providers\\Http\\DTO\\RequestOptions' ),
+ 'request_options_used' => false,
+ 'curl_hook_installed' => false,
+ );
+ }
+
/**
* Assemble a provider request without dispatching it.
*
diff --git a/inc/Engine/AI/RequestInspector.php b/inc/Engine/AI/RequestInspector.php
index 07d831832..f393ece3f 100644
--- a/inc/Engine/AI/RequestInspector.php
+++ b/inc/Engine/AI/RequestInspector.php
@@ -142,7 +142,7 @@ public function inspectPipelineJob( int $job_id, ?string $flow_step_id = null ):
$provider = (string) $mode_model['provider'];
$model = (string) $mode_model['model'];
- $assembled = RequestBuilder::assemble(
+ $assembled = RequestBuilder::assemble(
$messages,
$provider,
$model,
@@ -150,6 +150,12 @@ public function inspectPipelineJob( int $job_id, ?string $flow_step_id = null ):
ToolPolicyResolver::MODE_PIPELINE,
$payload
);
+ $transport_profile = RequestBuilder::wpAiClientTransportProfile(
+ ToolPolicyResolver::MODE_PIPELINE,
+ $provider,
+ $model,
+ $payload
+ );
return array_merge(
array(
@@ -160,6 +166,7 @@ public function inspectPipelineJob( int $job_id, ?string $flow_step_id = null ):
'provider' => $provider,
'model' => $model,
'mode' => ToolPolicyResolver::MODE_PIPELINE,
+ 'transport' => $transport_profile,
),
$this->measure( $assembled, $data_packets, $messages, $packet_projection_context )
);
diff --git a/tests/Unit/Abilities/SettingsAbilitiesTest.php b/tests/Unit/Abilities/SettingsAbilitiesTest.php
index d5010bc12..bd377a831 100644
--- a/tests/Unit/Abilities/SettingsAbilitiesTest.php
+++ b/tests/Unit/Abilities/SettingsAbilitiesTest.php
@@ -99,10 +99,28 @@ public function test_get_settings_contains_expected_keys(): void {
$this->assertArrayHasKey( 'default_provider', $settings );
$this->assertArrayHasKey( 'default_model', $settings );
$this->assertArrayHasKey( 'max_turns', $settings );
+ $this->assertArrayHasKey( 'wp_ai_client_connect_timeout', $settings );
+ $this->assertArrayHasKey( 'wp_ai_client_request_timeout', $settings );
$this->assertArrayHasKey( 'disabled_tools', $settings );
$this->assertArrayHasKey( 'ai_provider_keys', $settings );
}
+ public function test_update_settings_clamps_wp_ai_client_timeouts(): void {
+ $result = $this->settings_abilities->executeUpdateSettings(
+ array(
+ 'wp_ai_client_connect_timeout' => 999,
+ 'wp_ai_client_request_timeout' => 9999,
+ )
+ );
+
+ $this->assertTrue( $result['success'] );
+ $this->assertArrayNotHasKey( 'unhandled_keys', $result );
+
+ $updated_settings = get_option( 'datamachine_settings', array() );
+ $this->assertSame( 300.0, $updated_settings['wp_ai_client_connect_timeout'] );
+ $this->assertSame( 900.0, $updated_settings['wp_ai_client_request_timeout'] );
+ }
+
public function test_update_settings_updates_boolean_setting(): void {
$result = $this->settings_abilities->executeUpdateSettings(
array( 'cleanup_job_data_on_failure' => false )
diff --git a/tests/wp-ai-client-request-timeout-smoke.php b/tests/wp-ai-client-request-timeout-smoke.php
index 5d3468c0e..cd0d2bc79 100644
--- a/tests/wp-ai-client-request-timeout-smoke.php
+++ b/tests/wp-ai-client-request-timeout-smoke.php
@@ -9,7 +9,9 @@
declare(strict_types=1);
-$GLOBALS['datamachine_timeout_test_filters'] = array();
+$GLOBALS['datamachine_timeout_test_filters'] = array();
+$GLOBALS['datamachine_timeout_test_actions'] = array();
+$GLOBALS['datamachine_timeout_test_settings'] = array();
if ( ! function_exists( 'add_filter' ) ) {
function add_filter( string $tag, callable $callback, int $priority = 10, int $accepted_args = 1 ): void {
@@ -53,6 +55,7 @@ function apply_filters( string $tag, $value, ...$args ) {
if ( ! function_exists( 'do_action' ) ) {
function do_action( string $tag, ...$args ): void {
$GLOBALS['datamachine_timeout_test_last_action'] = array( $tag, $args );
+ $GLOBALS['datamachine_timeout_test_actions'][] = array( $tag, $args );
}
}
@@ -76,7 +79,9 @@ function sanitize_key( string $key ): string {
if ( ! function_exists( 'get_option' ) ) {
function get_option( string $option, $default = false ) {
- unset( $option );
+ if ( 'datamachine_settings' === $option ) {
+ return $GLOBALS['datamachine_timeout_test_settings'];
+ }
return $default;
}
}
@@ -204,6 +209,10 @@ function timeout_smoke_filter_count( string $tag ): int {
$timeout_context = null;
$connect_timeout_context = null;
$GLOBALS['datamachine_test_wp_ai_client_model_with_request_options'] = true;
+$GLOBALS['datamachine_timeout_test_settings'] = array(
+ 'wp_ai_client_connect_timeout' => 25.0,
+ 'wp_ai_client_request_timeout' => 180.0,
+);
add_filter(
'datamachine_wp_ai_client_request_timeout',
@@ -225,7 +234,8 @@ function ( float $timeout, string $mode, string $provider, string $model, array
6
);
-$result = RequestBuilder::build(
+$request_metadata = null;
+$result = RequestBuilder::build(
array(
array(
'role' => 'user',
@@ -244,12 +254,13 @@ function ( float $timeout, string $mode, string $provider, string $model, array
'gpt-smoke',
array(),
'pipeline',
- array( 'job_id' => 1695 )
+ array( 'job_id' => 1695, 'flow_step_id' => 'ai-step-1' ),
+ $request_metadata
);
assert_timeout_smoke( ! is_wp_error( $result ), 'RequestBuilder dispatch succeeds with wp-ai-client test double' );
assert_timeout_smoke( 240.0 === ( TimeoutPromptBuilderDouble::$captured_request['timeout'] ?? null ), 'Data Machine applies scoped wp-ai-client request timeout' );
-assert_timeout_smoke( 300.0 === ( $timeout_context['timeout'] ?? null ), 'Data Machine timeout filter receives product default' );
+assert_timeout_smoke( 180.0 === ( $timeout_context['timeout'] ?? null ), 'Data Machine timeout filter receives configured request timeout default' );
assert_timeout_smoke( 'pipeline' === ( $timeout_context['mode'] ?? null ), 'Data Machine timeout filter receives execution mode' );
assert_timeout_smoke( 'openai' === ( $timeout_context['provider'] ?? null ), 'Data Machine timeout filter receives provider' );
assert_timeout_smoke( 'gpt-smoke' === ( $timeout_context['model'] ?? null ), 'Data Machine timeout filter receives model' );
@@ -265,7 +276,7 @@ function ( float $timeout, string $mode, string $provider, string $model, array
assert_timeout_smoke( 240.0 === $captured_builder_request_options?->getTimeout(), 'Data Machine sets PromptBuilder RequestOptions timeout from scoped request timeout' );
assert_timeout_smoke( 120.0 === $captured_builder_request_options?->getConnectTimeout(), 'Data Machine sets PromptBuilder RequestOptions connect timeout from scoped connect timeout' );
-assert_timeout_smoke( 30.0 === ( $connect_timeout_context['timeout'] ?? null ), 'Data Machine connect timeout filter receives product default' );
+assert_timeout_smoke( 25.0 === ( $connect_timeout_context['timeout'] ?? null ), 'Data Machine connect timeout filter receives configured connect timeout default' );
assert_timeout_smoke( 240.0 === ( $connect_timeout_context['request_timeout'] ?? null ), 'Data Machine connect timeout filter receives resolved request timeout' );
assert_timeout_smoke( 'pipeline' === ( $connect_timeout_context['mode'] ?? null ), 'Data Machine connect timeout filter receives execution mode' );
assert_timeout_smoke( 'openai' === ( $connect_timeout_context['provider'] ?? null ), 'Data Machine connect timeout filter receives provider' );
@@ -278,6 +289,25 @@ function ( float $timeout, string $mode, string $provider, string $model, array
assert_timeout_smoke( 'continue' === $captured_prompt, 'Data Machine passes latest user message as wp-ai-client prompt' );
assert_timeout_smoke( 1 === ( TimeoutPromptBuilderDouble::$captured_request['curl_filter_count'] ?? null ), 'Data Machine scopes cURL low-speed settings during wp-ai-client dispatch' );
+$transport = $request_metadata['transport'] ?? array();
+assert_timeout_smoke( 240.0 === ( $transport['request_timeout'] ?? null ), 'Request metadata includes resolved request timeout' );
+assert_timeout_smoke( 120.0 === ( $transport['connect_timeout'] ?? null ), 'Request metadata includes resolved connect timeout' );
+assert_timeout_smoke( true === ( $transport['request_options_class_available'] ?? null ), 'Request metadata includes RequestOptions availability' );
+assert_timeout_smoke( true === ( $transport['request_options_used'] ?? null ), 'Request metadata includes RequestOptions usage' );
+assert_timeout_smoke( true === ( $transport['curl_hook_installed'] ?? null ), 'Request metadata includes cURL hook installation state' );
+assert_timeout_smoke( 'ai-step-1' === ( $transport['flow_step_id'] ?? null ), 'Request metadata includes flow step id' );
+
+$transport_logs = array_values(
+ array_filter(
+ $GLOBALS['datamachine_timeout_test_actions'],
+ static fn( $action ) => 'datamachine_log' === ( $action[0] ?? null ) && 'AI transport profile resolved' === ( $action[1][1] ?? null )
+ )
+);
+assert_timeout_smoke( 1 === count( $transport_logs ), 'Data Machine logs resolved AI transport profile before dispatch' );
+$transport_log_context = $transport_logs[0][1][2] ?? array();
+assert_timeout_smoke( 240.0 === ( $transport_log_context['request_timeout'] ?? null ), 'Transport log includes resolved request timeout' );
+assert_timeout_smoke( 120.0 === ( $transport_log_context['connect_timeout'] ?? null ), 'Transport log includes resolved connect timeout' );
+
$request_builder_source = file_get_contents( __DIR__ . '/../inc/Engine/AI/RequestBuilder.php' );
assert_timeout_smoke( is_string( $request_builder_source ) && str_contains( $request_builder_source, 'CURLOPT_CONNECTTIMEOUT' ), 'Data Machine scopes cURL connect timeout during wp-ai-client dispatch' );
assert_timeout_smoke( is_string( $request_builder_source ) && str_contains( $request_builder_source, 'use ( $request_timeout, $connect_timeout )' ), 'Data Machine cURL timeout hook receives request and connect timeouts' );
|