From 716572d0b9a9c3dd4d289f254e964b005a053372 Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Tue, 19 Aug 2025 16:46:54 -0300 Subject: [PATCH 01/26] Add base HTTPClient --- includes/Classifai/Providers/HTTPClient.php | 368 ++++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 includes/Classifai/Providers/HTTPClient.php diff --git a/includes/Classifai/Providers/HTTPClient.php b/includes/Classifai/Providers/HTTPClient.php new file mode 100644 index 000000000..6d848ab89 --- /dev/null +++ b/includes/Classifai/Providers/HTTPClient.php @@ -0,0 +1,368 @@ +api_key = $api_key; + $this->feature = $feature; + } + + /** + * Makes an authorized GET request. + * + * @since x.x.x + * + * @param string $url The API URL. + * @param array $options Additional query params. + * @return array|WP_Error + */ + public function get( string $url, array $options = [] ) { + /** + * Filter the URL for the get request. + * + * @since x.x.x + * @hook classifai_{provider}_api_request_get_url + * + * @param {string} $url The URL for the request. + * @param {array} $options The options for the request. + * @param {string} $this->feature The feature name. + * + * @return {string} The URL for the request. + */ + $url = apply_filters( $this->get_filter_prefix() . '_api_request_get_url', $url, $options, $this->feature ); + + /** + * Filter the options for the get request. + * + * @since x.x.x + * @hook classifai_{provider}_api_request_get_options + * + * @param {array} $options The options for the request. + * @param {string} $url The URL for the request. + * @param {string} $this->feature The feature name. + * + * @return {array} The options for the request. + */ + $options = apply_filters( $this->get_filter_prefix() . '_api_request_get_options', $options, $url, $this->feature ); + + $this->add_headers( $options ); + + /** + * Filter the response from the provider for a get request. + * + * @since x.x.x + * @hook classifai_{provider}_api_response_get + * + * @param {array|WP_Error} $response The API response. + * @param {string} $url Request URL. + * @param {array} $options Request body options. + * @param {string} $this->feature Feature name. + * + * @return {array} API response. + */ + return apply_filters( + $this->get_filter_prefix() . '_api_response_get', + $this->get_result( wp_remote_get( $url, $options ) ), // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get + $url, + $options, + $this->feature + ); + } + + /** + * Makes an authorized POST request. + * + * @since x.x.x + * + * @param string $url The API URL. + * @param array $options Additional query params. + * @return array|WP_Error + */ + public function post( string $url = '', array $options = [] ) { + $options = wp_parse_args( + $options, + [ + 'timeout' => $this->get_default_timeout(), + ] + ); + + /** + * Filter the URL for the post request. + * + * @since x.x.x + * @hook classifai_{provider}_api_request_post_url + * + * @param {string} $url The URL for the request. + * @param {array} $options The options for the request. + * @param {string} $this->feature The feature name. + * + * @return {string} The URL for the request. + */ + $url = apply_filters( $this->get_filter_prefix() . '_api_request_post_url', $url, $options, $this->feature ); + + /** + * Filter the options for the post request. + * + * @since x.x.x + * @hook classifai_{provider}_api_request_post_options + * + * @param {array} $options The options for the request. + * @param {string} $url The URL for the request. + * @param {string} $this->feature The feature name. + * + * @return {array} The options for the request. + */ + $options = apply_filters( $this->get_filter_prefix() . '_api_request_post_options', $options, $url, $this->feature ); + + $this->add_headers( $options ); + + /** + * Filter the response from the provider for a post request. + * + * @since x.x.x + * @hook classifai_{provider}_api_response_post + * + * @param {array|WP_Error} $response The API response. + * @param {string} $url Request URL. + * @param {array} $options Request body options. + * @param {string} $this->feature Feature name. + * + * @return {array} API response. + */ + return apply_filters( + $this->get_filter_prefix() . '_api_response_post', + $this->get_result( wp_remote_post( $url, $options ) ), // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get + $url, + $options, + $this->feature + ); + } + + /** + * Get results from the response. + * + * @since x.x.x + * + * @param array|WP_Error $response The API response. + * @return array|WP_Error + */ + public function get_result( $response ) { + if ( is_wp_error( $response ) ) { + return $response; + } + + return $this->parse_response( $response ); + } + + /** + * Parse the response from the API. + * + * @since x.x.x + * + * @param array $response The API response + * @return array|WP_Error + */ + protected function parse_response( array $response ) { + $code = wp_remote_retrieve_response_code( $response ); + $body = wp_remote_retrieve_body( $response ); + + // Error responses + if ( $code >= 400 ) { + $response_message = wp_remote_retrieve_response_message( $response ); + $status_text = $response_message ?: __( 'Unknown error', 'classifai' ); + + // Try to extract specific error message from the server response + $server_response = $this->extract_error_message( $response ); + + if ( ! empty( $server_response ) ) { + $error_message = sprintf( + /* translators: %1$d is the HTTP status code, %2$s is the HTTP status message, %3$s is the server response */ + __( 'An error occurred when performing an HTTP request: %1$d (%2$s). Server response: [ %3$s ]', 'classifai' ), + $code, + $status_text, + $server_response + ); + } else { + $error_message = sprintf( + /* translators: %1$d is the HTTP status code, %2$s is the HTTP status message */ + __( 'An error occurred when performing an HTTP request: %1$d (%2$s).', 'classifai' ), + $code, + $status_text + ); + } + + return new WP_Error( $code, $error_message ); + } + + // Successful responses + $headers = wp_remote_retrieve_headers( $response ); + $content_type = false; + + if ( ! empty( $headers ) ) { + $content_type = isset( $headers['content-type'] ) ? $headers['content-type'] : false; + } + + // JSON responses + if ( false === $content_type || false !== strpos( $content_type, 'application/json' ) ) { + $json = json_decode( $body, true ); + + if ( json_last_error() === JSON_ERROR_NONE ) { + if ( isset( $json['error'] ) && $json['error'] ) { + $error_message = $this->extract_error_message( $response ); + return new WP_Error( 'api_error', $error_message ?: __( 'API returned an error', 'classifai' ) ); + } + + return $json; + } else { + $error_msg = __( 'Invalid JSON response: ', 'classifai' ) . json_last_error_msg(); + return new WP_Error( 'invalid_json', $error_msg, [ + 'body' => wp_is_stream( $body ) ? null : wp_html_excerpt( (string) $body, 1000, '…' ), + ] ); + } + } else { + $error_msg = __( 'Unsupported response content type', 'classifai' ); + return new WP_Error( 'invalid_content_type', $error_msg, [ + 'content_type' => $content_type, + 'body' => wp_is_stream( $body ) ? null : wp_html_excerpt( (string) $body, 1000, '…' ), + ] ); + } + } + + /** + * Add the headers. + * + * @since x.x.x + * + * @param array $options The header options, passed by reference. + */ + protected function add_headers( array &$options = [] ) { + if ( empty( $options['headers'] ) ) { + $options['headers'] = []; + } + + // Get provider-specific default headers + $default_headers = $this->get_default_headers(); + + // Merge default headers (only if not already set) + foreach ( $default_headers as $header => $value ) { + if ( ! isset( $options['headers'][ $header ] ) ) { + $options['headers'][ $header ] = $value; + } + } + + // Add authentication header (this may override default) + $this->add_auth_header( $options ); + } + + /** + * Get the API key. + * + * @since x.x.x + * + * @return string + */ + public function get_api_key(): string { + return $this->api_key; + } + + /** + * Add authentication header. + * + * @since x.x.x + * @param array $options The header options, passed by reference. + */ + protected function add_auth_header( array &$options ) { + // Child override + } + + /** + * Get default headers for this provider. + * + * @since x.x.x + * @return array Default headers to include in requests. + */ + protected function get_default_headers(): array { + return [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ]; + } + + /** + * Get the filter prefix for this provider. + * + * @since x.x.x + * @return string + */ + abstract protected function get_filter_prefix(): string; + + /** + * Extract error message from response body. + * + * @since x.x.x + * @param array $response The API response. + * @return string + */ + protected function extract_error_message( array $response ): string { + $body = wp_remote_retrieve_body( $response ); + $headers = wp_remote_retrieve_headers( $response ); + $content_type = isset( $headers['content-type'] ) ? $headers['content-type'] : false; + + if ( $content_type && false !== strpos( $content_type, 'application/json' ) ) { + $json = json_decode( $body, true ); + if ( json_last_error() === JSON_ERROR_NONE && ! empty( $json['error'] ) ) { + $provider_error = is_string( $json['error'] ) + ? $json['error'] + : ( $json['error']['message'] ?? '' ); + + if ( ! empty( $provider_error ) ) { + return $provider_error; + } + } + } + + return ''; + } + + /** + * Get the default timeout for requests. + * + * @since x.x.x + * @return int + */ + protected function get_default_timeout(): int { + return 90; + } + +} From f8ac479277b776ec1f8640ce9bef06ca7d59f590 Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Tue, 19 Aug 2025 17:36:09 -0300 Subject: [PATCH 02/26] Refactor GoogleAI APIRequest to extend HTTPClient, unify auth and filters --- .../Providers/GoogleAI/APIRequest.php | 198 ++---------------- 1 file changed, 13 insertions(+), 185 deletions(-) diff --git a/includes/Classifai/Providers/GoogleAI/APIRequest.php b/includes/Classifai/Providers/GoogleAI/APIRequest.php index 3d2d8118f..86893b80b 100644 --- a/includes/Classifai/Providers/GoogleAI/APIRequest.php +++ b/includes/Classifai/Providers/GoogleAI/APIRequest.php @@ -2,7 +2,7 @@ namespace Classifai\Providers\GoogleAI; -use WP_Error; +use Classifai\Providers\HTTPClient; /** * The APIRequest class is a low level class to make Google AI API @@ -16,206 +16,34 @@ * $request = new Classifai\Providers\GoogleAI\APIRequest(); * $request->post( $googleai_url, $options ); */ -class APIRequest { - - /** - * The Google AI API key. - * - * @var string - */ - public $api_key; - - /** - * The feature name. - * - * @var string - */ - public $feature; +class APIRequest extends HTTPClient { /** * Google AI APIRequest constructor. * - * @param string $api_key Google AI API key. + * @param string $api_key API key. * @param string $feature Feature name. */ public function __construct( string $api_key = '', string $feature = '' ) { - $this->api_key = $api_key; - $this->feature = $feature; + parent::__construct( $api_key, $feature ); } /** - * Makes an authorized GET request. + * Get the filter prefix for this provider. * - * @param string $url The Google AI API url - * @param array $options Additional query params - * @return array|WP_Error - */ - public function get( string $url, array $options = [] ) { - /** - * Filter the URL for the get request. - * - * @since 3.0.0 - * @hook classifai_googleai_api_request_get_url - * - * @param {string} $url The URL for the request. - * @param {array} $options The options for the request. - * @param {string} $this->feature The feature name. - * - * @return {string} The URL for the request. - */ - $url = apply_filters( 'classifai_googleai_api_request_get_url', $url, $options, $this->feature ); - - /** - * Filter the options for the get request. - * - * @since 3.0.0 - * @hook classifai_googleai_api_request_get_options - * - * @param {array} $options The options for the request. - * @param {string} $url The URL for the request. - * @param {string} $this->feature The feature name. - * - * @return {array} The options for the request. - */ - $options = apply_filters( 'classifai_googleai_api_request_get_options', $options, $url, $this->feature ); - - $this->add_headers( $options ); - - /** - * Filter the response from Google AI for a get request. - * - * @since 3.0.0 - * @hook classifai_googleai_api_response_get - * - * @param {array|WP_Error} $response API response. - * @param {string} $url Request URL. - * @param {array} $options Request body options. - * @param {string} $this->feature Feature name. - * - * @return {array} API response. - */ - return apply_filters( - 'classifai_googleai_api_response_get', - $this->get_result( wp_remote_get( $url, $options ) ), // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get - $url, - $options, - $this->feature - ); - } - - /** - * Makes an authorized POST request. - * - * @param string $url The Google AI API URL. - * @param array $options Additional query params. - * @return array|WP_Error - */ - public function post( string $url = '', array $options = [] ) { - $options = wp_parse_args( - $options, - [ - 'timeout' => 90, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout - ] - ); - - /** - * Filter the URL for the post request. - * - * @since 3.0.0 - * @hook classifai_googleai_api_request_post_url - * - * @param {string} $url The URL for the request. - * @param {array} $options The options for the request. - * @param {string} $this->feature The feature name. - * - * @return {string} The URL for the request. - */ - $url = apply_filters( 'classifai_googleai_api_request_post_url', $url, $options, $this->feature ); - - /** - * Filter the options for the post request. - * - * @since 3.0.0 - * @hook classifai_googleai_api_request_post_options - * - * @param {array} $options The options for the request. - * @param {string} $url The URL for the request. - * @param {string} $this->feature The feature name. - * - * @return {array} The options for the request. - */ - $options = apply_filters( 'classifai_googleai_api_request_post_options', $options, $url, $this->feature ); - - $this->add_headers( $options ); - - /** - * Filter the response from Google AI for a post request. - * - * @since 3.0.0 - * @hook classifai_googleai_api_response_post - * - * @param {array|WP_Error} $response API response. - * @param {string} $url Request URL. - * @param {array} $options Request body options. - * @param {string} $this->feature Feature name. - * - * @return {array} API response. - */ - return apply_filters( - 'classifai_googleai_api_response_post', - $this->get_result( wp_remote_post( $url, $options ) ), // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get - $url, - $options, - $this->feature - ); - } - - /** - * Get results from the response. - * - * @param object $response The API response. - * @return array|WP_Error + * @return string */ - public function get_result( $response ) { - if ( ! is_wp_error( $response ) ) { - $body = wp_remote_retrieve_body( $response ); - $code = wp_remote_retrieve_response_code( $response ); - $json = json_decode( $body, true ); - - if ( json_last_error() === JSON_ERROR_NONE ) { - if ( empty( $json['error'] ) ) { - return $json; - } else { - $message = $json['error']['message'] ?? esc_html__( 'An error occurred', 'classifai' ); - return new WP_Error( $code, $message ); - } - } elseif ( ! empty( wp_remote_retrieve_response_message( $response ) ) ) { - return new WP_Error( $code, wp_remote_retrieve_response_message( $response ) ); - } else { - return new WP_Error( 'Invalid JSON: ' . json_last_error_msg(), $body ); - } - } else { - return $response; - } + protected function get_filter_prefix(): string { + return 'classifai_googleai'; } /** - * Add the headers. + * Add authentication header. * * @param array $options The header options, passed by reference. */ - public function add_headers( array &$options = [] ) { - if ( empty( $options['headers'] ) ) { - $options['headers'] = []; - } - - if ( ! isset( $options['headers']['x-goog-api-key'] ) ) { - $options['headers']['x-goog-api-key'] = $this->get_auth_header(); - } - - if ( ! isset( $options['headers']['Content-Type'] ) ) { - $options['headers']['Content-Type'] = 'application/json'; - } + protected function add_auth_header( array &$options ) { + $options['headers']['x-goog-api-key'] = $this->get_auth_header(); } /** @@ -223,7 +51,7 @@ public function add_headers( array &$options = [] ) { * * @return string */ - public function get_auth_header() { + public function get_auth_header(): string { return $this->get_api_key(); } @@ -232,7 +60,7 @@ public function get_auth_header() { * * @return string */ - public function get_api_key() { + public function get_api_key(): string { return $this->api_key; } } From d3aab22390f1bffaf446041f43ff9a6e33208ae4 Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Tue, 19 Aug 2025 18:07:12 -0300 Subject: [PATCH 03/26] Simplify OpenAI APIRequest via HTTPClient, keep audio parsing for raw responses --- .../Classifai/Providers/OpenAI/APIRequest.php | 215 +++--------------- 1 file changed, 27 insertions(+), 188 deletions(-) diff --git a/includes/Classifai/Providers/OpenAI/APIRequest.php b/includes/Classifai/Providers/OpenAI/APIRequest.php index 09b987d0e..ec6ab6c92 100644 --- a/includes/Classifai/Providers/OpenAI/APIRequest.php +++ b/includes/Classifai/Providers/OpenAI/APIRequest.php @@ -2,6 +2,7 @@ namespace Classifai\Providers\OpenAI; +use Classifai\Providers\HTTPClient; use WP_Error; /** @@ -16,158 +17,25 @@ * $request = new Classifai\Providers\OpenAI\APIRequest(); * $request->post( $openai_url, $options ); */ -class APIRequest { - - /** - * The OpenAI API key. - * - * @var string - */ - public $api_key; - - /** - * The feature name. - * - * @var string - */ - public $feature; +class APIRequest extends HTTPClient { /** * OpenAI APIRequest constructor. * - * @param string $api_key OpenAI API key. + * @param string $api_key API key. * @param string $feature Feature name. */ public function __construct( string $api_key = '', string $feature = '' ) { - $this->api_key = $api_key; - $this->feature = $feature; + parent::__construct( $api_key, $feature ); } /** - * Makes an authorized GET request. + * Get the filter prefix for this provider. * - * @param string $url The OpenAI API url - * @param array $options Additional query params - * @return array|WP_Error - */ - public function get( string $url, array $options = [] ) { - /** - * Filter the URL for the get request. - * - * @since 2.4.0 - * @hook classifai_openai_api_request_get_url - * - * @param {string} $url The URL for the request. - * @param {array} $options The options for the request. - * @param {string} $this->feature The feature name. - * - * @return {string} The URL for the request. - */ - $url = apply_filters( 'classifai_openai_api_request_get_url', $url, $options, $this->feature ); - - /** - * Filter the options for the get request. - * - * @since 2.4.0 - * @hook classifai_openai_api_request_get_options - * - * @param {array} $options The options for the request. - * @param {string} $url The URL for the request. - * @param {string} $this->feature The feature name. - * - * @return {array} The options for the request. - */ - $options = apply_filters( 'classifai_openai_api_request_get_options', $options, $url, $this->feature ); - - $this->add_headers( $options ); - - /** - * Filter the response from OpenAI for a get request. - * - * @since 2.4.0 - * @hook classifai_openai_api_response_get - * - * @param {array|WP_Error} $response The API response. - * @param {string} $url Request URL. - * @param {array} $options Request body options. - * @param {string} $this->feature Feature name. - * - * @return {array} API response. - */ - return apply_filters( - 'classifai_openai_api_response_get', - $this->get_result( wp_remote_get( $url, $options ) ), // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get - $url, - $options, - $this->feature - ); - } - - /** - * Makes an authorized POST request. - * - * @param string $url The OpenAI API URL. - * @param array $options Additional query params. - * @return array|WP_Error + * @return string */ - public function post( string $url = '', array $options = [] ) { - $options = wp_parse_args( - $options, - [ - 'timeout' => 90, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout - ] - ); - - /** - * Filter the URL for the post request. - * - * @since 2.4.0 - * @hook classifai_openai_api_request_post_url - * - * @param {string} $url The URL for the request. - * @param {array} $options The options for the request. - * @param {string} $this->feature The feature name. - * - * @return {string} The URL for the request. - */ - $url = apply_filters( 'classifai_openai_api_request_post_url', $url, $options, $this->feature ); - - /** - * Filter the options for the post request. - * - * @since 2.4.0 - * @hook classifai_openai_api_request_post_options - * - * @param {array} $options The options for the request. - * @param {string} $url The URL for the request. - * @param {string} $this->feature The feature name. - * - * @return {array} The options for the request. - */ - $options = apply_filters( 'classifai_openai_api_request_post_options', $options, $url, $this->feature ); - - $this->add_headers( $options ); - - /** - * Filter the response from OpenAI for a post request. - * - * @since 2.4.0 - * @hook classifai_openai_api_response_post - * - * @param {array|WP_Error} $response The API response. - * @param {string} $url Request URL. - * @param {array} $options Request body options. - * @param {string} $this->feature Feature name. - * - * @return {array} API response. - */ - return apply_filters( - 'classifai_openai_api_response_post', - $this->get_result( wp_remote_post( $url, $options ) ), // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get - $url, - $options, - $this->feature - ); + protected function get_filter_prefix(): string { + return 'classifai_openai'; } /** @@ -177,7 +45,7 @@ public function post( string $url = '', array $options = [] ) { * @param array $body The body of the request. * @return array|WP_Error */ - public function post_form( string $url = '', array $body = [] ) { + public function post_form( $url = '', $body = [] ) { /** * Filter the URL for the post form request. * @@ -266,63 +134,34 @@ public function post_form( string $url = '', array $body = [] ) { } /** - * Get results from the response. + * Parse the response from the API with special handling for audio content. * - * @param object $response The API response. + * @param array $response The API response. * @return array|WP_Error */ - public function get_result( $response ) { - if ( ! is_wp_error( $response ) ) { - $headers = wp_remote_retrieve_headers( $response ); - $content_type = false; - - if ( ! empty( $headers ) ) { - $content_type = isset( $headers['content-type'] ) ? $headers['content-type'] : false; - } + protected function parse_response( array $response ) { + $headers = wp_remote_retrieve_headers( $response ); + $content_type = false; - $body = wp_remote_retrieve_body( $response ); - $code = wp_remote_retrieve_response_code( $response ); - - if ( false === $content_type || false !== strpos( $content_type, 'application/json' ) ) { - $json = json_decode( $body, true ); + if ( ! empty( $headers ) ) { + $content_type = isset( $headers['content-type'] ) ? $headers['content-type'] : false; + } - if ( json_last_error() === JSON_ERROR_NONE ) { - if ( empty( $json['error'] ) ) { - return $json; - } else { - $message = $json['error']['message'] ?? esc_html__( 'An error occurred', 'classifai' ); - return new WP_Error( $code, $message ); - } - } else { - return new WP_Error( 'Invalid JSON: ' . json_last_error_msg(), $body ); - } - } elseif ( $content_type && false !== strpos( $content_type, 'audio/mpeg' ) ) { - return $response; - } else { - return new WP_Error( 'Invalid content type', $response ); - } - } else { - return $response; + // Special handling for OpenAI audio so it doesn't try to parse as JSON. + if ( $content_type && false !== strpos( $content_type, 'audio/mpeg' ) ) { + return $response; // Raw response } + + return parent::parse_response( $response ); } /** - * Add the headers. + * Add authentication header. * * @param array $options The header options, passed by reference. */ - public function add_headers( array &$options = [] ) { - if ( empty( $options['headers'] ) ) { - $options['headers'] = []; - } - - if ( ! isset( $options['headers']['Authorization'] ) ) { - $options['headers']['Authorization'] = $this->get_auth_header(); - } - - if ( ! isset( $options['headers']['Content-Type'] ) ) { - $options['headers']['Content-Type'] = 'application/json'; - } + protected function add_auth_header( array &$options ) { + $options['headers']['Authorization'] = $this->get_auth_header(); } /** @@ -330,7 +169,7 @@ public function add_headers( array &$options = [] ) { * * @return string */ - public function get_auth_header() { + public function get_auth_header(): string { return 'Bearer ' . $this->get_api_key(); } @@ -339,7 +178,7 @@ public function get_auth_header() { * * @return string */ - public function get_api_key() { + public function get_api_key(): string { return $this->api_key; } } From b38d0d21f8def709fd7b6bd75bea06e59a1e4d89 Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Tue, 19 Aug 2025 18:14:53 -0300 Subject: [PATCH 04/26] Guard null scheduler in embeddings check to prevent fatal error --- includes/Classifai/Providers/OpenAI/Embeddings.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/includes/Classifai/Providers/OpenAI/Embeddings.php b/includes/Classifai/Providers/OpenAI/Embeddings.php index 0a1a56ae5..4949ccbb3 100644 --- a/includes/Classifai/Providers/OpenAI/Embeddings.php +++ b/includes/Classifai/Providers/OpenAI/Embeddings.php @@ -1728,6 +1728,10 @@ public function get_debug_information(): array { * @return bool */ public function is_embeddings_generation_in_progress(): bool { + if ( null === self::$scheduler_instance ) { + return false; + } + if ( $this->feature_instance instanceof Classification ) { return self::$scheduler_instance->is_embeddings_generation_in_progress( 'classifai_generate_term_embedding_job' ); } From dd7ebc6d9ff34bd89d63ea292ebf6171c758dac4 Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Tue, 19 Aug 2025 18:34:00 -0300 Subject: [PATCH 05/26] Migrate xAI APIRequest to HTTPClient with custom timeout handling --- .../Classifai/Providers/XAI/APIRequest.php | 197 ++---------------- 1 file changed, 17 insertions(+), 180 deletions(-) diff --git a/includes/Classifai/Providers/XAI/APIRequest.php b/includes/Classifai/Providers/XAI/APIRequest.php index 4e95a8e5e..fbb2e5d98 100644 --- a/includes/Classifai/Providers/XAI/APIRequest.php +++ b/includes/Classifai/Providers/XAI/APIRequest.php @@ -2,6 +2,7 @@ namespace Classifai\Providers\XAI; +use Classifai\Providers\HTTPClient; use WP_Error; /** @@ -16,207 +17,43 @@ * $request = new Classifai\Providers\XAI\APIRequest(); * $request->post( $xai_url, $options ); */ -class APIRequest { - - /** - * The xAI API key. - * - * @var string - */ - public $api_key; - - /** - * The feature name. - * - * @var string - */ - public $feature; +class APIRequest extends HTTPClient { /** * xAI APIRequest constructor. * - * @param string $api_key xAI API key. + * @param string $api_key API key. * @param string $feature Feature name. */ public function __construct( string $api_key = '', string $feature = '' ) { - $this->api_key = $api_key; - $this->feature = $feature; - } - - /** - * Makes an authorized GET request. - * - * @param string $url The xAI API url - * @param array $options Additional query params - * @return array|WP_Error - */ - public function get( string $url, array $options = [] ) { - /** - * Filter the URL for the get request. - * - * @since 3.3.0 - * @hook classifai_xai_api_request_get_url - * - * @param {string} $url The URL for the request. - * @param {array} $options The options for the request. - * @param {string} $this->feature The feature name. - * - * @return {string} The URL for the request. - */ - $url = apply_filters( 'classifai_xai_api_request_get_url', $url, $options, $this->feature ); - - /** - * Filter the options for the get request. - * - * @since 3.3.0 - * @hook classifai_xai_api_request_get_options - * - * @param {array} $options The options for the request. - * @param {string} $url The URL for the request. - * @param {string} $this->feature The feature name. - * - * @return {array} The options for the request. - */ - $options = apply_filters( 'classifai_xai_api_request_get_options', $options, $url, $this->feature ); - - $this->add_headers( $options ); - - /** - * Filter the response from xAI for a get request. - * - * @since 3.3.0 - * @hook classifai_xai_api_response_get - * - * @param {array|WP_Error} $response The API response. - * @param {string} $url Request URL. - * @param {array} $options Request body options. - * @param {string} $this->feature Feature name. - * - * @return {array} API response. - */ - return apply_filters( - 'classifai_xai_api_response_get', - $this->get_result( wp_remote_get( $url, $options ) ), // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get - $url, - $options, - $this->feature - ); + parent::__construct( $api_key, $feature ); } /** - * Makes an authorized POST request. + * Get the filter prefix for this provider. * - * @param string $url The xAI API URL. - * @param array $options Additional query params. - * @return array|WP_Error + * @return string */ - public function post( string $url = '', array $options = [] ) { - $options = wp_parse_args( - $options, - [ - 'timeout' => 60, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout - ] - ); - - /** - * Filter the URL for the post request. - * - * @since 3.3.0 - * @hook classifai_xai_api_request_post_url - * - * @param {string} $url The URL for the request. - * @param {array} $options The options for the request. - * @param {string} $this->feature The feature name. - * - * @return {string} The URL for the request. - */ - $url = apply_filters( 'classifai_xai_api_request_post_url', $url, $options, $this->feature ); - - /** - * Filter the options for the post request. - * - * @since 3.3.0 - * @hook classifai_xai_api_request_post_options - * - * @param {array} $options The options for the request. - * @param {string} $url The URL for the request. - * @param {string} $this->feature The feature name. - * - * @return {array} The options for the request. - */ - $options = apply_filters( 'classifai_xai_api_request_post_options', $options, $url, $this->feature ); - - $this->add_headers( $options ); - - /** - * Filter the response from xAI for a post request. - * - * @since 3.3.0 - * @hook classifai_xai_api_response_post - * - * @param {array|WP_Error} $response The API response. - * @param {string} $url Request URL. - * @param {array} $options Request body options. - * @param {string} $this->feature Feature name. - * - * @return {array} API response. - */ - return apply_filters( - 'classifai_xai_api_response_post', - $this->get_result( wp_remote_post( $url, $options ) ), // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get - $url, - $options, - $this->feature - ); + protected function get_filter_prefix(): string { + return 'classifai_xai'; } /** - * Get results from the response. + * Get the default timeout for xAI requests. * - * @param object $response The API response. - * @return array|WP_Error + * @return int */ - public function get_result( $response ) { - if ( ! is_wp_error( $response ) ) { - $body = wp_remote_retrieve_body( $response ); - $code = wp_remote_retrieve_response_code( $response ); - $json = json_decode( $body, true ); - - if ( json_last_error() === JSON_ERROR_NONE ) { - if ( empty( $json['error'] ) ) { - return $json; - } else { - $message = $json['error']['message'] ?? ''; - if ( empty( $message ) ) { - $message = $json['error'] ?? esc_html__( 'An error occurred', 'classifai' ); - } - return new WP_Error( $code, $message ); - } - } else { - return new WP_Error( 'Invalid JSON: ' . json_last_error_msg(), $body ); - } - } else { - return $response; - } + protected function get_default_timeout(): int { + return 60; } /** - * Add the headers. + * Add authentication header. * * @param array $options The header options, passed by reference. */ - public function add_headers( array &$options = [] ) { - if ( empty( $options['headers'] ) ) { - $options['headers'] = []; - } - - if ( ! isset( $options['headers']['Authorization'] ) ) { - $options['headers']['Authorization'] = $this->get_auth_header(); - } - - if ( ! isset( $options['headers']['Content-Type'] ) ) { - $options['headers']['Content-Type'] = 'application/json'; - } + protected function add_auth_header( array &$options ) { + $options['headers']['Authorization'] = $this->get_auth_header(); } /** @@ -224,7 +61,7 @@ public function add_headers( array &$options = [] ) { * * @return string */ - public function get_auth_header() { + public function get_auth_header(): string { return 'Bearer ' . $this->get_api_key(); } @@ -233,7 +70,7 @@ public function get_auth_header() { * * @return string */ - public function get_api_key() { + public function get_api_key(): string { return $this->api_key; } } From 9c8cfe0d391481f1ace2fe7821a2fcb4913b2541 Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Tue, 19 Aug 2025 19:02:52 -0300 Subject: [PATCH 06/26] Refactor Watson APIRequest to use HTTPClient with basic auth support --- .../Classifai/Providers/Watson/APIRequest.php | 79 +++++++------------ 1 file changed, 29 insertions(+), 50 deletions(-) diff --git a/includes/Classifai/Providers/Watson/APIRequest.php b/includes/Classifai/Providers/Watson/APIRequest.php index 382563015..10b14642f 100644 --- a/includes/Classifai/Providers/Watson/APIRequest.php +++ b/includes/Classifai/Providers/Watson/APIRequest.php @@ -2,6 +2,8 @@ namespace Classifai\Providers\Watson; +use Classifai\Providers\HTTPClient; +use WP_Error; use function Classifai\Providers\Watson\get_username; use function Classifai\Providers\Watson\get_password; @@ -18,7 +20,7 @@ * $request = new APIRequest(); * $request->post( $nlu_url, $options ); */ -class APIRequest { +class APIRequest extends HTTPClient { /** * The Watson API username. @@ -35,67 +37,50 @@ class APIRequest { public $password; /** - * Adds authorization headers to the request options and makes an - * HTTP request. The result is parsed and returned if valid JSON. + * Watson APIRequest constructor. * - * @param string $url The Watson API url - * @param array $options Additional query params - * @return array|\WP_Error + * @param string $api_key Not used for Watson, kept for compatibility. + * @param string $feature Feature name. */ - public function request( string $url, array $options = [] ) { - $this->add_headers( $options ); - return $this->get_result( wp_remote_request( $url, $options ) ); + public function __construct( string $api_key = '', string $feature = '' ) { + parent::__construct( $api_key, $feature ); + $this->username = get_username(); + $this->password = get_password(); } /** - * Makes an authorized GET request and returns the parsed JSON - * response if valid. + * Get the filter prefix for this provider. * - * @param string $url The Watson API url - * @param array $options Additional query params - * @return array|\WP_Error + * @return string */ - public function get( string $url, array $options = [] ) { - $this->add_headers( $options ); - return $this->get_result( wp_remote_get( $url, $options ) ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get + protected function get_filter_prefix(): string { + return 'classifai_watson'; } /** - * Makes an authorized POST request and returns the parsed JSON - * response if valid. + * Adds authorization headers to the request options and makes an + * HTTP request. The result is parsed and returned if valid JSON. * * @param string $url The Watson API url * @param array $options Additional query params - * @return array|\WP_Error + * @return array|WP_Error */ - public function post( string $url, array $options = [] ) { + public function request( string $url, array $options = [] ) { $this->add_headers( $options ); - return $this->get_result( wp_remote_post( $url, $options ) ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get + return $this->get_result( wp_remote_request( $url, $options ) ); } /** - * Get results from the response. + * Makes an authorized GET request and returns the parsed JSON + * response if valid. * - * @param object $response The API response. - * @return array|\WP_Error + * @param string $url The Watson API url + * @param array $options Additional query params + * @return array|WP_Error */ - public function get_result( $response ) { - if ( ! is_wp_error( $response ) ) { - $body = wp_remote_retrieve_body( $response ); - $json = json_decode( $body, true ); - - if ( json_last_error() === JSON_ERROR_NONE ) { - if ( empty( $json['error'] ) ) { - return $json; - } else { - return new \WP_Error( $json['code'], $json['error'] ); - } - } else { - return new \WP_Error( 'Invalid JSON: ' . json_last_error_msg(), $body ); - } - } else { - return $response; - } + public function get( string $url, array $options = [] ) { + // Ensure filter hooks are called + return parent::get( $url, $options ); } /** @@ -146,17 +131,11 @@ public function get_auth_hash(): string { } /** - * Add the headers. + * Add authentication header. * * @param array $options The header options, passed by reference. */ - public function add_headers( array &$options ) { - if ( empty( $options['headers'] ) ) { - $options['headers'] = []; - } - + protected function add_auth_header( array &$options ) { $options['headers']['Authorization'] = $this->get_auth_header(); - $options['headers']['Accept'] = 'application/json'; - $options['headers']['Content-Type'] = 'application/json'; } } From 6a1f1cf22da7038fa1d7a2a5f2fafae4b57b2a39 Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Tue, 19 Aug 2025 19:13:15 -0300 Subject: [PATCH 07/26] Update Watson Classifier to import new APIRequest --- includes/Classifai/Providers/Watson/Classifier.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/includes/Classifai/Providers/Watson/Classifier.php b/includes/Classifai/Providers/Watson/Classifier.php index 686193f98..228a646f9 100644 --- a/includes/Classifai/Providers/Watson/Classifier.php +++ b/includes/Classifai/Providers/Watson/Classifier.php @@ -2,6 +2,8 @@ namespace Classifai\Providers\Watson; +use Classifai\Providers\Watson\APIRequest; + /** * The Classifier object uses the IBM Watson NLU API to classify plain * text into NLU features. The low level API Request object is used here From 4bd76a53af407f2a892f252fb6cdc8a2068a64e7 Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Tue, 19 Aug 2025 19:27:14 -0300 Subject: [PATCH 08/26] Wire Watson NLU to new APIRequest for consistent requests --- includes/Classifai/Providers/Watson/NLU.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/Classifai/Providers/Watson/NLU.php b/includes/Classifai/Providers/Watson/NLU.php index 43d347d7f..6fd0f727d 100644 --- a/includes/Classifai/Providers/Watson/NLU.php +++ b/includes/Classifai/Providers/Watson/NLU.php @@ -10,6 +10,7 @@ use Classifai\Features\Classification; use Classifai\Features\Feature; use Classifai\Providers\Watson\PostClassifier; +use Classifai\Providers\Watson\APIRequest; use WP_Error; use function Classifai\get_classification_feature_taxonomy; From d485b65753314e8bd6a311440965e9acc638703b Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Tue, 19 Aug 2025 19:42:47 -0300 Subject: [PATCH 09/26] Switch Watson PostClassifier to new APIRequest for classification --- includes/Classifai/Providers/Watson/PostClassifier.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/Classifai/Providers/Watson/PostClassifier.php b/includes/Classifai/Providers/Watson/PostClassifier.php index d2dfb4189..44a289d6e 100644 --- a/includes/Classifai/Providers/Watson/PostClassifier.php +++ b/includes/Classifai/Providers/Watson/PostClassifier.php @@ -4,6 +4,7 @@ use Classifai\Features\Classification; use Classifai\Providers\Watson\NLU; +use Classifai\Providers\Watson\APIRequest; use Classifai\Normalizer; /** From a12dbe9830b3d7290b95489fc4eed163f6b77d05 Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Tue, 19 Aug 2025 20:04:18 -0300 Subject: [PATCH 10/26] Point Ollama provider to Localhost APIRequest instead of OpenAI --- includes/Classifai/Providers/Localhost/Ollama.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/Classifai/Providers/Localhost/Ollama.php b/includes/Classifai/Providers/Localhost/Ollama.php index e2864f73a..57e50dfa1 100644 --- a/includes/Classifai/Providers/Localhost/Ollama.php +++ b/includes/Classifai/Providers/Localhost/Ollama.php @@ -6,7 +6,7 @@ namespace Classifai\Providers\Localhost; use Classifai\Providers\Provider; -use Classifai\Providers\OpenAI\APIRequest; +use Classifai\Providers\Localhost\APIRequest; use Classifai\Features\ContentResizing; use Classifai\Features\ExcerptGeneration; use Classifai\Features\TitleGeneration; From 0c9a252c26e9abc05beccd26c2548c0cbdf4ca14 Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Tue, 19 Aug 2025 20:16:11 -0300 Subject: [PATCH 11/26] Use Localhost APIRequest in OllamaEmbeddings to decouple from OpenAI --- includes/Classifai/Providers/Localhost/OllamaEmbeddings.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/Classifai/Providers/Localhost/OllamaEmbeddings.php b/includes/Classifai/Providers/Localhost/OllamaEmbeddings.php index af7a7ea41..c85ab6a1b 100644 --- a/includes/Classifai/Providers/Localhost/OllamaEmbeddings.php +++ b/includes/Classifai/Providers/Localhost/OllamaEmbeddings.php @@ -7,7 +7,7 @@ use Classifai\Admin\Notifications; use Classifai\Features\Classification; -use Classifai\Providers\OpenAI\APIRequest; +use Classifai\Providers\Localhost\APIRequest; use Classifai\Providers\OpenAI\EmbeddingCalculations; use Classifai\Providers\OpenAI\Tokenizer; use Classifai\Features\Feature; From 546b4c9f670c99f03ee29a65fb81b1bc9e18c59c Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Tue, 19 Aug 2025 20:31:02 -0300 Subject: [PATCH 12/26] Use Localhost APIRequest for OllamaMultimodal provider --- includes/Classifai/Providers/Localhost/OllamaMultimodal.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/Classifai/Providers/Localhost/OllamaMultimodal.php b/includes/Classifai/Providers/Localhost/OllamaMultimodal.php index ea587971a..7fba84356 100644 --- a/includes/Classifai/Providers/Localhost/OllamaMultimodal.php +++ b/includes/Classifai/Providers/Localhost/OllamaMultimodal.php @@ -8,7 +8,7 @@ use Classifai\Features\DescriptiveTextGenerator; use Classifai\Features\ImageTagsGenerator; use Classifai\Features\ImageTextExtraction; -use Classifai\Providers\OpenAI\APIRequest; +use Classifai\Providers\Localhost\APIRequest; use WP_Error; use function Classifai\get_largest_size_and_dimensions_image_url; From 6760b4c1221b483f505641df4dbc0d473b34ed27 Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Tue, 19 Aug 2025 20:48:19 -0300 Subject: [PATCH 13/26] Migrate StableDiffusion to Localhost APIRequest --- includes/Classifai/Providers/Localhost/StableDiffusion.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/Classifai/Providers/Localhost/StableDiffusion.php b/includes/Classifai/Providers/Localhost/StableDiffusion.php index cd2e50d57..873f011d2 100644 --- a/includes/Classifai/Providers/Localhost/StableDiffusion.php +++ b/includes/Classifai/Providers/Localhost/StableDiffusion.php @@ -6,7 +6,7 @@ namespace Classifai\Providers\Localhost; use Classifai\Providers\Provider; -use Classifai\Providers\OpenAI\APIRequest; +use Classifai\Providers\Localhost\APIRequest; use Classifai\Features\ImageGeneration; use WP_Error; From 93f13d128daca24ef8c33a97add3b5346a80b2cb Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Tue, 19 Aug 2025 21:03:26 -0300 Subject: [PATCH 14/26] Update TogetherAI Images to use its own APIRequest --- includes/Classifai/Providers/TogetherAI/Images.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/Classifai/Providers/TogetherAI/Images.php b/includes/Classifai/Providers/TogetherAI/Images.php index ce8bc194b..1bef2523c 100644 --- a/includes/Classifai/Providers/TogetherAI/Images.php +++ b/includes/Classifai/Providers/TogetherAI/Images.php @@ -6,7 +6,7 @@ namespace Classifai\Providers\TogetherAI; use Classifai\Providers\Provider; -use Classifai\Providers\OpenAI\APIRequest; +use Classifai\Providers\TogetherAI\APIRequest; use Classifai\Features\ImageGeneration; use WP_Error; From 32810aed4ba1f7af1befa42a30628d6d1424d8ca Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Tue, 19 Aug 2025 21:19:15 -0300 Subject: [PATCH 15/26] Switch TogetherAI trait to its own APIRequest --- includes/Classifai/Providers/TogetherAI/TogetherAI.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/Classifai/Providers/TogetherAI/TogetherAI.php b/includes/Classifai/Providers/TogetherAI/TogetherAI.php index a71797077..b4313d261 100644 --- a/includes/Classifai/Providers/TogetherAI/TogetherAI.php +++ b/includes/Classifai/Providers/TogetherAI/TogetherAI.php @@ -5,7 +5,7 @@ namespace Classifai\Providers\TogetherAI; -use Classifai\Providers\OpenAI\APIRequest; +use Classifai\Providers\TogetherAI\APIRequest; use WP_Error; trait TogetherAI { From fcc5c4790f4bb07bfac1f4faa7a4309b9177dda5 Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Tue, 19 Aug 2025 22:01:21 -0300 Subject: [PATCH 16/26] Add LocalHost Provider APIRequest --- .../Providers/Localhost/APIRequest.php | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 includes/Classifai/Providers/Localhost/APIRequest.php diff --git a/includes/Classifai/Providers/Localhost/APIRequest.php b/includes/Classifai/Providers/Localhost/APIRequest.php new file mode 100644 index 000000000..3178aefed --- /dev/null +++ b/includes/Classifai/Providers/Localhost/APIRequest.php @@ -0,0 +1,68 @@ +post( $localhost_url, $options ); + */ +class APIRequest extends HTTPClient { + + /** + * Localhost APIRequest constructor. + * + * @param string $api_key API key (not used for localhost, kept for compatibility). + * @param string $feature Feature name. + */ + public function __construct( string $api_key = '', string $feature = '' ) { + parent::__construct( $api_key, $feature ); + } + + /** + * Get the filter prefix for this provider. + * + * @return string + */ + protected function get_filter_prefix(): string { + return 'classifai_localhost'; + } + + /** + * Add authentication header. + * + * @param array $options The header options, passed by reference. + */ + protected function add_auth_header( array &$options ) { + // Not needed for localhost providers. + } + + /** + * Get the auth header. + * + * @return string + */ + public function get_auth_header() { + // Not needed for localhost providers. + return ''; + } + + /** + * Get the Localhost API key. + * + * @return string + */ + public function get_api_key():string { + return $this->api_key; + } +} From de170cd21e0367235295aa540ec388183ba9a0d2 Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Tue, 19 Aug 2025 22:17:50 -0300 Subject: [PATCH 17/26] Add TogetherAI Provider APIRequest --- .../Providers/TogetherAI/APIRequest.php | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 includes/Classifai/Providers/TogetherAI/APIRequest.php diff --git a/includes/Classifai/Providers/TogetherAI/APIRequest.php b/includes/Classifai/Providers/TogetherAI/APIRequest.php new file mode 100644 index 000000000..21ee3b56f --- /dev/null +++ b/includes/Classifai/Providers/TogetherAI/APIRequest.php @@ -0,0 +1,67 @@ +post( $togetherai_url, $options ); + */ +class APIRequest extends HTTPClient { + + /** + * TogetherAI APIRequest constructor. + * + * @param string $api_key API key. + * @param string $feature Feature name. + */ + public function __construct( string $api_key = '', string $feature = '' ) { + parent::__construct( $api_key, $feature ); + } + + /** + * Get the filter prefix for this provider. + * + * @return string + */ + protected function get_filter_prefix(): string { + return 'classifai_togetherai'; + } + + /** + * Add authentication header. + * + * @param array $options The header options, passed by reference. + */ + protected function add_auth_header( array &$options ) { + $options['headers']['Authorization'] = $this->get_auth_header(); + } + + /** + * Get the auth header. + * + * @return string + */ + public function get_auth_header(): string { + return 'Bearer ' . $this->get_api_key(); + } + + /** + * Get the TogetherAI API key. + * + * @return string + */ + public function get_api_key(): string { + return $this->api_key; + } +} From 66e6c95cb78f6989363430de2851203aef8f94af Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Mon, 25 Aug 2025 14:29:18 -0300 Subject: [PATCH 18/26] Adjust after merge --- includes/Classifai/Providers/Localhost/APIRequest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/includes/Classifai/Providers/Localhost/APIRequest.php b/includes/Classifai/Providers/Localhost/APIRequest.php index 3178aefed..0b0515df2 100644 --- a/includes/Classifai/Providers/Localhost/APIRequest.php +++ b/includes/Classifai/Providers/Localhost/APIRequest.php @@ -3,7 +3,6 @@ namespace Classifai\Providers\Localhost; use Classifai\Providers\HTTPClient; -use WP_Error; /** * The APIRequest class is a low level class to make Localhost API @@ -62,7 +61,7 @@ public function get_auth_header() { * * @return string */ - public function get_api_key():string { + public function get_api_key(): string { return $this->api_key; } } From 3023fc969221c4f3ef5a95cda59ef154ed0f22df Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Mon, 25 Aug 2025 14:30:43 -0300 Subject: [PATCH 19/26] Remove unused code added after merge --- includes/Classifai/Providers/OpenAI/APIRequest.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/includes/Classifai/Providers/OpenAI/APIRequest.php b/includes/Classifai/Providers/OpenAI/APIRequest.php index 1ab4244fa..d7e5036a8 100644 --- a/includes/Classifai/Providers/OpenAI/APIRequest.php +++ b/includes/Classifai/Providers/OpenAI/APIRequest.php @@ -3,9 +3,7 @@ namespace Classifai\Providers\OpenAI; use Classifai\Providers\HTTPClient; -use WP_Error; use function Classifai\safe_wp_remote_post; -use function Classifai\safe_wp_remote_get; /** * The APIRequest class is a low level class to make OpenAI API @@ -47,7 +45,7 @@ protected function get_filter_prefix(): string { * @param array $body The body of the request. * @return array|WP_Error */ - public function post_form( $url = '', $body = [] ) { + public function post_form( string $url = '', array $body = [] ) { /** * Filter the URL for the post form request. * From 7a3edeaae43a21a6473f7a303ec38be9c8021214 Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Mon, 25 Aug 2025 14:31:14 -0300 Subject: [PATCH 20/26] Remove unused code added after merge --- includes/Classifai/Providers/TogetherAI/APIRequest.php | 1 - includes/Classifai/Providers/XAI/APIRequest.php | 3 --- 2 files changed, 4 deletions(-) diff --git a/includes/Classifai/Providers/TogetherAI/APIRequest.php b/includes/Classifai/Providers/TogetherAI/APIRequest.php index 21ee3b56f..a8a1248d5 100644 --- a/includes/Classifai/Providers/TogetherAI/APIRequest.php +++ b/includes/Classifai/Providers/TogetherAI/APIRequest.php @@ -3,7 +3,6 @@ namespace Classifai\Providers\TogetherAI; use Classifai\Providers\HTTPClient; -use WP_Error; /** * The APIRequest class is a low level class to make TogetherAI API diff --git a/includes/Classifai/Providers/XAI/APIRequest.php b/includes/Classifai/Providers/XAI/APIRequest.php index 6a8682af9..513bcd319 100644 --- a/includes/Classifai/Providers/XAI/APIRequest.php +++ b/includes/Classifai/Providers/XAI/APIRequest.php @@ -3,9 +3,6 @@ namespace Classifai\Providers\XAI; use Classifai\Providers\HTTPClient; -use WP_Error; -use function Classifai\safe_wp_remote_get; -use function Classifai\safe_wp_remote_post; /** * The APIRequest class is a low level class to make xAI API From b12ba0b351ef07395ff402e24e9b53b9ff3316d3 Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Mon, 25 Aug 2025 14:32:12 -0300 Subject: [PATCH 21/26] Remove unused code added after merge --- .../Classifai/Providers/Watson/APIRequest.php | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/includes/Classifai/Providers/Watson/APIRequest.php b/includes/Classifai/Providers/Watson/APIRequest.php index 0d2ca74c8..018fbce04 100644 --- a/includes/Classifai/Providers/Watson/APIRequest.php +++ b/includes/Classifai/Providers/Watson/APIRequest.php @@ -3,12 +3,8 @@ namespace Classifai\Providers\Watson; use Classifai\Providers\HTTPClient; -use WP_Error; use function Classifai\Providers\Watson\get_username; use function Classifai\Providers\Watson\get_password; -use function Classifai\safe_wp_remote_get; -use function Classifai\safe_wp_remote_post; -use function Classifai\safe_wp_remote_request; /** * APIRequest class is the low level class to make IBM Watson API @@ -60,31 +56,6 @@ protected function get_filter_prefix(): string { return 'classifai_watson'; } - /** - * Adds authorization headers to the request options and makes an - * HTTP request. The result is parsed and returned if valid JSON. - * - * @param string $url The Watson API url - * @param array $options Additional query params - * @return array|WP_Error - */ - public function request( string $url, array $options = [] ) { - $this->add_headers( $options ); - - $method = strtoupper( $options['method'] ?? 'GET' ); - - if ( 'GET' === $method ) { - return $this->get_result( safe_wp_remote_get( $url, $options ) ); - } - - if ( 'POST' === $method ) { - return $this->get_result( safe_wp_remote_post( $url, $options ) ); - } - - // Fallback for other HTTP verbs. - return $this->get_result( safe_wp_remote_request( $method, $url, $options ) ); - } - /** * Makes an authorized GET request and returns the parsed JSON * response if valid. From 68212256ba9afb91796d1f2eb24dd4a07a3cadc3 Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Mon, 25 Aug 2025 14:47:10 -0300 Subject: [PATCH 22/26] Skimming non-specific constructors --- includes/Classifai/Providers/GoogleAI/APIRequest.php | 10 ---------- includes/Classifai/Providers/Localhost/APIRequest.php | 10 ---------- includes/Classifai/Providers/OpenAI/APIRequest.php | 10 ---------- includes/Classifai/Providers/TogetherAI/APIRequest.php | 10 ---------- includes/Classifai/Providers/XAI/APIRequest.php | 10 ---------- 5 files changed, 50 deletions(-) diff --git a/includes/Classifai/Providers/GoogleAI/APIRequest.php b/includes/Classifai/Providers/GoogleAI/APIRequest.php index 86893b80b..ec84104dd 100644 --- a/includes/Classifai/Providers/GoogleAI/APIRequest.php +++ b/includes/Classifai/Providers/GoogleAI/APIRequest.php @@ -18,16 +18,6 @@ */ class APIRequest extends HTTPClient { - /** - * Google AI APIRequest constructor. - * - * @param string $api_key API key. - * @param string $feature Feature name. - */ - public function __construct( string $api_key = '', string $feature = '' ) { - parent::__construct( $api_key, $feature ); - } - /** * Get the filter prefix for this provider. * diff --git a/includes/Classifai/Providers/Localhost/APIRequest.php b/includes/Classifai/Providers/Localhost/APIRequest.php index 0b0515df2..dc4a734ad 100644 --- a/includes/Classifai/Providers/Localhost/APIRequest.php +++ b/includes/Classifai/Providers/Localhost/APIRequest.php @@ -18,16 +18,6 @@ */ class APIRequest extends HTTPClient { - /** - * Localhost APIRequest constructor. - * - * @param string $api_key API key (not used for localhost, kept for compatibility). - * @param string $feature Feature name. - */ - public function __construct( string $api_key = '', string $feature = '' ) { - parent::__construct( $api_key, $feature ); - } - /** * Get the filter prefix for this provider. * diff --git a/includes/Classifai/Providers/OpenAI/APIRequest.php b/includes/Classifai/Providers/OpenAI/APIRequest.php index d7e5036a8..f4e676824 100644 --- a/includes/Classifai/Providers/OpenAI/APIRequest.php +++ b/includes/Classifai/Providers/OpenAI/APIRequest.php @@ -19,16 +19,6 @@ */ class APIRequest extends HTTPClient { - /** - * OpenAI APIRequest constructor. - * - * @param string $api_key API key. - * @param string $feature Feature name. - */ - public function __construct( string $api_key = '', string $feature = '' ) { - parent::__construct( $api_key, $feature ); - } - /** * Get the filter prefix for this provider. * diff --git a/includes/Classifai/Providers/TogetherAI/APIRequest.php b/includes/Classifai/Providers/TogetherAI/APIRequest.php index a8a1248d5..8130370df 100644 --- a/includes/Classifai/Providers/TogetherAI/APIRequest.php +++ b/includes/Classifai/Providers/TogetherAI/APIRequest.php @@ -18,16 +18,6 @@ */ class APIRequest extends HTTPClient { - /** - * TogetherAI APIRequest constructor. - * - * @param string $api_key API key. - * @param string $feature Feature name. - */ - public function __construct( string $api_key = '', string $feature = '' ) { - parent::__construct( $api_key, $feature ); - } - /** * Get the filter prefix for this provider. * diff --git a/includes/Classifai/Providers/XAI/APIRequest.php b/includes/Classifai/Providers/XAI/APIRequest.php index 513bcd319..e925aea11 100644 --- a/includes/Classifai/Providers/XAI/APIRequest.php +++ b/includes/Classifai/Providers/XAI/APIRequest.php @@ -18,16 +18,6 @@ */ class APIRequest extends HTTPClient { - /** - * xAI APIRequest constructor. - * - * @param string $api_key API key. - * @param string $feature Feature name. - */ - public function __construct( string $api_key = '', string $feature = '' ) { - parent::__construct( $api_key, $feature ); - } - /** * Get the filter prefix for this provider. * From 4e33acca23300be60ebe00d3d3966dbe87aa3fd7 Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Mon, 25 Aug 2025 14:51:31 -0300 Subject: [PATCH 23/26] Linter fixes --- includes/Classifai/Providers/HTTPClient.php | 33 +++++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/includes/Classifai/Providers/HTTPClient.php b/includes/Classifai/Providers/HTTPClient.php index 6d848ab89..6d293202d 100644 --- a/includes/Classifai/Providers/HTTPClient.php +++ b/includes/Classifai/Providers/HTTPClient.php @@ -200,7 +200,7 @@ protected function parse_response( array $response ) { // Error responses if ( $code >= 400 ) { $response_message = wp_remote_retrieve_response_message( $response ); - $status_text = $response_message ?: __( 'Unknown error', 'classifai' ); + $status_text = $response_message ? $response_message : __( 'Unknown error', 'classifai' ); // Try to extract specific error message from the server response $server_response = $this->extract_error_message( $response ); @@ -226,7 +226,7 @@ protected function parse_response( array $response ) { } // Successful responses - $headers = wp_remote_retrieve_headers( $response ); + $headers = wp_remote_retrieve_headers( $response ); $content_type = false; if ( ! empty( $headers ) ) { @@ -240,22 +240,30 @@ protected function parse_response( array $response ) { if ( json_last_error() === JSON_ERROR_NONE ) { if ( isset( $json['error'] ) && $json['error'] ) { $error_message = $this->extract_error_message( $response ); - return new WP_Error( 'api_error', $error_message ?: __( 'API returned an error', 'classifai' ) ); + return new WP_Error( 'api_error', $error_message ? $error_message : __( 'API returned an error', 'classifai' ) ); } return $json; } else { $error_msg = __( 'Invalid JSON response: ', 'classifai' ) . json_last_error_msg(); - return new WP_Error( 'invalid_json', $error_msg, [ - 'body' => wp_is_stream( $body ) ? null : wp_html_excerpt( (string) $body, 1000, '…' ), - ] ); + return new WP_Error( + 'invalid_json', + $error_msg, + [ + 'body' => wp_is_stream( $body ) ? null : wp_html_excerpt( (string) $body, 1000, '…' ), + ] + ); } } else { $error_msg = __( 'Unsupported response content type', 'classifai' ); - return new WP_Error( 'invalid_content_type', $error_msg, [ - 'content_type' => $content_type, - 'body' => wp_is_stream( $body ) ? null : wp_html_excerpt( (string) $body, 1000, '…' ), - ] ); + return new WP_Error( + 'invalid_content_type', + $error_msg, + [ + 'content_type' => $content_type, + 'body' => wp_is_stream( $body ) ? null : wp_html_excerpt( (string) $body, 1000, '…' ), + ] + ); } } @@ -335,8 +343,8 @@ abstract protected function get_filter_prefix(): string; * @return string */ protected function extract_error_message( array $response ): string { - $body = wp_remote_retrieve_body( $response ); - $headers = wp_remote_retrieve_headers( $response ); + $body = wp_remote_retrieve_body( $response ); + $headers = wp_remote_retrieve_headers( $response ); $content_type = isset( $headers['content-type'] ) ? $headers['content-type'] : false; if ( $content_type && false !== strpos( $content_type, 'application/json' ) ) { @@ -364,5 +372,4 @@ protected function extract_error_message( array $response ): string { protected function get_default_timeout(): int { return 90; } - } From 136e401d7f79eaedec01eec236033ea486de6253 Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Mon, 25 Aug 2025 14:58:12 -0300 Subject: [PATCH 24/26] Make add_headers method public to be used by unit tests --- includes/Classifai/Providers/HTTPClient.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/Classifai/Providers/HTTPClient.php b/includes/Classifai/Providers/HTTPClient.php index 6d293202d..5a6a2f408 100644 --- a/includes/Classifai/Providers/HTTPClient.php +++ b/includes/Classifai/Providers/HTTPClient.php @@ -274,7 +274,7 @@ protected function parse_response( array $response ) { * * @param array $options The header options, passed by reference. */ - protected function add_headers( array &$options = [] ) { + public function add_headers( array &$options = [] ) { if ( empty( $options['headers'] ) ) { $options['headers'] = []; } From c98be9bb4e4367e8067413b7066f25b4883a3ec0 Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Mon, 25 Aug 2025 14:58:48 -0300 Subject: [PATCH 25/26] Create fresh APIRequest instances in 2 failing unit tests --- tests/Classifai/Watson/APIRequestTest.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/Classifai/Watson/APIRequestTest.php b/tests/Classifai/Watson/APIRequestTest.php index b5314eeb2..75abc78bf 100644 --- a/tests/Classifai/Watson/APIRequestTest.php +++ b/tests/Classifai/Watson/APIRequestTest.php @@ -29,7 +29,8 @@ function test_it_uses_constant_username_if_present() { function test_it_uses_option_username_if_present() { update_option( 'classifai_feature_classification', [ 'ibm_watson_nlu' => [ 'username' => 'foo-option' ] ] ); - $actual = $this->request->get_username(); + $request = new APIRequest(); + $actual = $request->get_username(); $this->assertEquals( 'foo-option', $actual ); } @@ -50,7 +51,8 @@ function test_it_constant_password_if_present() { function test_it_uses_option_password_if_present() { update_option( 'classifai_feature_classification', [ 'ibm_watson_nlu' => [ 'password' => 'foo-option' ] ] ); - $actual = $this->request->get_password(); + $request = new APIRequest(); + $actual = $request->get_password(); $this->assertEquals( 'foo-option', $actual ); } From b6644d72810b5301130509a7ce4bdbda161b7dbc Mon Sep 17 00:00:00 2001 From: Luiz Miguel Axcar Date: Mon, 25 Aug 2025 15:03:10 -0300 Subject: [PATCH 26/26] Remove non-specific override --- includes/Classifai/Providers/Watson/APIRequest.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/includes/Classifai/Providers/Watson/APIRequest.php b/includes/Classifai/Providers/Watson/APIRequest.php index 018fbce04..0f945f1b2 100644 --- a/includes/Classifai/Providers/Watson/APIRequest.php +++ b/includes/Classifai/Providers/Watson/APIRequest.php @@ -56,19 +56,6 @@ protected function get_filter_prefix(): string { return 'classifai_watson'; } - /** - * Makes an authorized GET request and returns the parsed JSON - * response if valid. - * - * @param string $url The Watson API url - * @param array $options Additional query params - * @return array|WP_Error - */ - public function get( string $url, array $options = [] ) { - // Ensure filter hooks are called - return parent::get( $url, $options ); - } - /** * Get the Watson username. *