diff --git a/includes/Classifai/Providers/GoogleAI/APIRequest.php b/includes/Classifai/Providers/GoogleAI/APIRequest.php index f5f696c9c..ec84104dd 100644 --- a/includes/Classifai/Providers/GoogleAI/APIRequest.php +++ b/includes/Classifai/Providers/GoogleAI/APIRequest.php @@ -2,9 +2,7 @@ namespace Classifai\Providers\GoogleAI; -use WP_Error; -use function Classifai\safe_wp_remote_get; -use function Classifai\safe_wp_remote_post; +use Classifai\Providers\HTTPClient; /** * The APIRequest class is a low level class to make Google AI API @@ -18,206 +16,24 @@ * $request = new Classifai\Providers\GoogleAI\APIRequest(); * $request->post( $googleai_url, $options ); */ -class APIRequest { +class APIRequest extends HTTPClient { /** - * The Google AI API key. + * Get the filter prefix for this provider. * - * @var string - */ - public $api_key; - - /** - * The feature name. - * - * @var string - */ - public $feature; - - /** - * Google AI APIRequest constructor. - * - * @param string $api_key Google AI 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 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( safe_wp_remote_get( $url, $options ) ), - $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( safe_wp_remote_post( $url, $options ) ), - $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(); } /** @@ -225,7 +41,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(); } @@ -234,7 +50,7 @@ public function get_auth_header() { * * @return string */ - public function get_api_key() { + public function get_api_key(): string { return $this->api_key; } } diff --git a/includes/Classifai/Providers/HTTPClient.php b/includes/Classifai/Providers/HTTPClient.php new file mode 100644 index 000000000..5a6a2f408 --- /dev/null +++ b/includes/Classifai/Providers/HTTPClient.php @@ -0,0 +1,375 @@ +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 ? $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 ? $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. + */ + public 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; + } +} diff --git a/includes/Classifai/Providers/Localhost/APIRequest.php b/includes/Classifai/Providers/Localhost/APIRequest.php new file mode 100644 index 000000000..dc4a734ad --- /dev/null +++ b/includes/Classifai/Providers/Localhost/APIRequest.php @@ -0,0 +1,57 @@ +post( $localhost_url, $options ); + */ +class APIRequest extends HTTPClient { + + /** + * 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; + } +} diff --git a/includes/Classifai/Providers/Localhost/Ollama.php b/includes/Classifai/Providers/Localhost/Ollama.php index 7576662a3..d1943dd5c 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; diff --git a/includes/Classifai/Providers/Localhost/OllamaEmbeddings.php b/includes/Classifai/Providers/Localhost/OllamaEmbeddings.php index e40b94386..bd31da110 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; diff --git a/includes/Classifai/Providers/Localhost/OllamaMultimodal.php b/includes/Classifai/Providers/Localhost/OllamaMultimodal.php index 111c40c8b..0b8537133 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; diff --git a/includes/Classifai/Providers/Localhost/StableDiffusion.php b/includes/Classifai/Providers/Localhost/StableDiffusion.php index a7503c6fd..cecfdf3dc 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; diff --git a/includes/Classifai/Providers/OpenAI/APIRequest.php b/includes/Classifai/Providers/OpenAI/APIRequest.php index 86681f533..f4e676824 100644 --- a/includes/Classifai/Providers/OpenAI/APIRequest.php +++ b/includes/Classifai/Providers/OpenAI/APIRequest.php @@ -2,9 +2,8 @@ namespace Classifai\Providers\OpenAI; -use WP_Error; +use Classifai\Providers\HTTPClient; 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 @@ -18,158 +17,15 @@ * $request = new Classifai\Providers\OpenAI\APIRequest(); * $request->post( $openai_url, $options ); */ -class APIRequest { +class APIRequest extends HTTPClient { /** - * The OpenAI API key. + * Get the filter prefix for this provider. * - * @var string - */ - public $api_key; - - /** - * The feature name. - * - * @var string - */ - public $feature; - - /** - * OpenAI APIRequest constructor. - * - * @param string $api_key OpenAI 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 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( safe_wp_remote_get( $url, $options ) ), - $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( safe_wp_remote_post( $url, $options ) ), - $url, - $options, - $this->feature - ); + protected function get_filter_prefix(): string { + return 'classifai_openai'; } /** @@ -268,63 +124,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(); } /** @@ -332,7 +159,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(); } @@ -341,7 +168,7 @@ public function get_auth_header() { * * @return string */ - public function get_api_key() { + public function get_api_key(): string { return $this->api_key; } } diff --git a/includes/Classifai/Providers/OpenAI/Embeddings.php b/includes/Classifai/Providers/OpenAI/Embeddings.php index ef273e15b..25f90254d 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' ); } diff --git a/includes/Classifai/Providers/TogetherAI/APIRequest.php b/includes/Classifai/Providers/TogetherAI/APIRequest.php new file mode 100644 index 000000000..8130370df --- /dev/null +++ b/includes/Classifai/Providers/TogetherAI/APIRequest.php @@ -0,0 +1,56 @@ +post( $togetherai_url, $options ); + */ +class APIRequest extends HTTPClient { + + /** + * 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; + } +} diff --git a/includes/Classifai/Providers/TogetherAI/Images.php b/includes/Classifai/Providers/TogetherAI/Images.php index d94d87f1f..7730d5566 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; diff --git a/includes/Classifai/Providers/TogetherAI/TogetherAI.php b/includes/Classifai/Providers/TogetherAI/TogetherAI.php index 1f36c89f7..4ffb7b6a3 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 { diff --git a/includes/Classifai/Providers/Watson/APIRequest.php b/includes/Classifai/Providers/Watson/APIRequest.php index 543bb37fd..0f945f1b2 100644 --- a/includes/Classifai/Providers/Watson/APIRequest.php +++ b/includes/Classifai/Providers/Watson/APIRequest.php @@ -2,11 +2,9 @@ namespace Classifai\Providers\Watson; +use Classifai\Providers\HTTPClient; 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 @@ -21,7 +19,7 @@ * $request = new APIRequest(); * $request->post( $nlu_url, $options ); */ -class APIRequest { +class APIRequest extends HTTPClient { /** * The Watson API username. @@ -38,79 +36,24 @@ 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 ); - - $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. - * - * @param string $url The Watson API url - * @param array $options Additional query params - * @return array|\WP_Error - */ - public function get( string $url, array $options = [] ) { - $this->add_headers( $options ); - return $this->get_result( safe_wp_remote_get( $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 POST 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 - */ - public function post( string $url, array $options = [] ) { - $this->add_headers( $options ); - return $this->get_result( safe_wp_remote_post( $url, $options ) ); - } - - /** - * 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 ); - $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; - } + protected function get_filter_prefix(): string { + return 'classifai_watson'; } /** @@ -161,17 +104,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'; } } diff --git a/includes/Classifai/Providers/Watson/Classifier.php b/includes/Classifai/Providers/Watson/Classifier.php index facb27647..0759d1533 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 diff --git a/includes/Classifai/Providers/Watson/NLU.php b/includes/Classifai/Providers/Watson/NLU.php index 67d82cf1e..cfd266bbe 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; 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; /** diff --git a/includes/Classifai/Providers/XAI/APIRequest.php b/includes/Classifai/Providers/XAI/APIRequest.php index fc9a29124..e925aea11 100644 --- a/includes/Classifai/Providers/XAI/APIRequest.php +++ b/includes/Classifai/Providers/XAI/APIRequest.php @@ -2,9 +2,7 @@ namespace Classifai\Providers\XAI; -use WP_Error; -use function Classifai\safe_wp_remote_get; -use function Classifai\safe_wp_remote_post; +use Classifai\Providers\HTTPClient; /** * The APIRequest class is a low level class to make xAI API @@ -18,207 +16,33 @@ * $request = new Classifai\Providers\XAI\APIRequest(); * $request->post( $xai_url, $options ); */ -class APIRequest { +class APIRequest extends HTTPClient { /** - * The xAI API key. + * Get the filter prefix for this provider. * - * @var string - */ - public $api_key; - - /** - * The feature name. - * - * @var string - */ - public $feature; - - /** - * xAI APIRequest constructor. - * - * @param string $api_key xAI 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( safe_wp_remote_get( $url, $options ) ), - $url, - $options, - $this->feature - ); - } - - /** - * Makes an authorized POST request. - * - * @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( safe_wp_remote_post( $url, $options ) ), - $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(); } /** @@ -226,7 +50,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(); } @@ -235,7 +59,7 @@ public function get_auth_header() { * * @return string */ - public function get_api_key() { + public function get_api_key(): string { return $this->api_key; } } 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 ); }