From 485cd6a44ecdc8f2e104e2d3391a4e0371815528 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Sat, 30 Aug 2025 12:37:30 -0700 Subject: [PATCH 01/21] feat: starts client adapter work --- .../class-wp-ai-client-client-adapter.php | 187 ++++++++++++++++++ .../class-wp-ai-client-network-exception.php | 49 +++++ 2 files changed, 236 insertions(+) create mode 100644 includes/http/class-wp-ai-client-client-adapter.php create mode 100644 includes/http/class-wp-ai-client-network-exception.php diff --git a/includes/http/class-wp-ai-client-client-adapter.php b/includes/http/class-wp-ai-client-client-adapter.php new file mode 100644 index 0000000..58cc7b9 --- /dev/null +++ b/includes/http/class-wp-ai-client-client-adapter.php @@ -0,0 +1,187 @@ +response_factory = $response_factory; + $this->stream_factory = $stream_factory; + } + + /** + * Sends a PSR-7 request and returns a PSR-7 response. + * + * @param RequestInterface $request The PSR-7 request. + * + * @return ResponseInterface The PSR-7 response. + * + * @throws ClientExceptionInterface If an error happens while processing the request. + */ + public function sendRequest( RequestInterface $request ): ResponseInterface { + $args = $this->prepare_wp_args( $request ); + $url = (string) $request->getUri(); + + $response = \wp_remote_request( $url, $args ); + + if ( \is_wp_error( $response ) ) { + throw new WP_AI_Client_Network_Exception( + $response->get_error_message(), + $request, + null, + $response->get_error_code() ? (int) $response->get_error_code() : 0 + ); + } + + return $this->create_psr_response( $response ); + } + + /** + * Prepare WordPress HTTP API arguments from PSR-7 request. + * + * @param RequestInterface $request The PSR-7 request. + * + * @return array WordPress HTTP API arguments. + */ + private function prepare_wp_args( RequestInterface $request ): array { + $args = array( + 'method' => $request->getMethod(), + 'headers' => $this->prepare_headers( $request ), + 'body' => $this->prepare_body( $request ), + 'timeout' => 30, + 'redirection' => 5, + 'httpversion' => $request->getProtocolVersion(), + 'blocking' => true, + ); + + // Handle streaming requests if needed. + if ( $request->hasHeader( 'X-Stream' ) ) { + $args['stream'] = true; + $args['filename'] = $request->getHeaderLine( 'X-Stream-Filename' ); + } + + return $args; + } + + /** + * Prepare headers for WordPress HTTP API. + * + * @param RequestInterface $request The PSR-7 request. + * + * @return array Headers array for WordPress HTTP API. + */ + private function prepare_headers( RequestInterface $request ): array { + $headers = array(); + + foreach ( $request->getHeaders() as $name => $values ) { + // Skip pseudo headers used for streaming. + if ( strpos( $name, 'X-Stream' ) === 0 ) { + continue; + } + + // WordPress expects headers as name => value pairs. + $headers[ $name ] = implode( ', ', $values ); + } + + return $headers; + } + + /** + * Prepare request body for WordPress HTTP API. + * + * @param RequestInterface $request The PSR-7 request. + * + * @return string|null The request body. + */ + private function prepare_body( RequestInterface $request ): ?string { + $body = $request->getBody(); + + if ( $body->getSize() === 0 ) { + return null; + } + + // Rewind the stream to ensure we read from the beginning. + if ( $body->isSeekable() ) { + $body->rewind(); + } + + return (string) $body; + } + + /** + * Create PSR-7 response from WordPress HTTP response. + * + * @param array $wp_response WordPress HTTP API response array. + * + * @return ResponseInterface PSR-7 response. + */ + private function create_psr_response( array $wp_response ): ResponseInterface { + $status_code = \wp_remote_retrieve_response_code( $wp_response ); + $reason_phrase = \wp_remote_retrieve_response_message( $wp_response ); + $headers = \wp_remote_retrieve_headers( $wp_response ); + $body = \wp_remote_retrieve_body( $wp_response ); + + // Create the PSR-7 response. + $response = $this->response_factory->createResponse( $status_code, $reason_phrase ); + + // Add headers to response. + if ( $headers instanceof \WP_HTTP_Requests_Response ) { + $headers = $headers->get_headers(); + } + + if ( is_array( $headers ) || $headers instanceof \ArrayAccess ) { + foreach ( $headers as $name => $value ) { + $response = $response->withHeader( $name, $value ); + } + } + + // Set the response body. + if ( ! empty( $body ) ) { + $stream = $this->stream_factory->createStream( $body ); + $response = $response->withBody( $stream ); + } + + return $response; + } +} \ No newline at end of file diff --git a/includes/http/class-wp-ai-client-network-exception.php b/includes/http/class-wp-ai-client-network-exception.php new file mode 100644 index 0000000..365cc6c --- /dev/null +++ b/includes/http/class-wp-ai-client-network-exception.php @@ -0,0 +1,49 @@ +request = $request; + } + + /** + * Returns the request. + * + * @return RequestInterface + */ + public function getRequest(): RequestInterface { + return $this->request; + } +} \ No newline at end of file From c06c4684567c8ad5d159bf029f0d495d0dae8882 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 9 Sep 2025 12:58:29 -0600 Subject: [PATCH 02/21] refactor: rename file to use PSR-4 autoloading --- ...er.php => WP_AI_Client_Client_Adapter.php} | 5 +- .../class-wp-ai-client-network-exception.php | 49 ------------------- 2 files changed, 2 insertions(+), 52 deletions(-) rename includes/http/{class-wp-ai-client-client-adapter.php => WP_AI_Client_Client_Adapter.php} (98%) delete mode 100644 includes/http/class-wp-ai-client-network-exception.php diff --git a/includes/http/class-wp-ai-client-client-adapter.php b/includes/http/WP_AI_Client_Client_Adapter.php similarity index 98% rename from includes/http/class-wp-ai-client-client-adapter.php rename to includes/http/WP_AI_Client_Client_Adapter.php index 58cc7b9..20ed3b7 100644 --- a/includes/http/class-wp-ai-client-client-adapter.php +++ b/includes/http/WP_AI_Client_Client_Adapter.php @@ -66,10 +66,9 @@ public function sendRequest( RequestInterface $request ): ResponseInterface { $response = \wp_remote_request( $url, $args ); if ( \is_wp_error( $response ) ) { - throw new WP_AI_Client_Network_Exception( + // TODO: Update to use PHP AI Client exceptions. + throw new \Exception( $response->get_error_message(), - $request, - null, $response->get_error_code() ? (int) $response->get_error_code() : 0 ); } diff --git a/includes/http/class-wp-ai-client-network-exception.php b/includes/http/class-wp-ai-client-network-exception.php deleted file mode 100644 index 365cc6c..0000000 --- a/includes/http/class-wp-ai-client-network-exception.php +++ /dev/null @@ -1,49 +0,0 @@ -request = $request; - } - - /** - * Returns the request. - * - * @return RequestInterface - */ - public function getRequest(): RequestInterface { - return $this->request; - } -} \ No newline at end of file From 845407ae604127ed0f735885c25cd9f3b99b54a4 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 9 Sep 2025 13:18:29 -0600 Subject: [PATCH 03/21] chore: adds WordPress stubs to PHPStan --- composer.json | 5 +- composer.lock | 119 ++++++++++++++++++++++++++++++++++++++++++++-- phpstan.neon.dist | 3 ++ 3 files changed, 122 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index dde2514..35057f4 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,8 @@ "phpstan/phpstan": "~2.1", "slevomat/coding-standard": "^8.0", "squizlabs/php_codesniffer": "^3.7", - "wp-coding-standards/wpcs": "^3.0" + "wp-coding-standards/wpcs": "^3.0", + "szepeviktor/phpstan-wordpress": "^2.0" }, "config": { "allow-plugins": { @@ -58,6 +59,6 @@ ], "phpcs": "phpcs", "phpcbf": "phpcbf", - "phpstan": "phpstan analyze --memory-limit=256M" + "phpstan": "phpstan analyze --memory-limit=512M" } } diff --git a/composer.lock b/composer.lock index 396adb7..a96741a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7dbba8d15fdd8095bb14f9df989589e1", + "content-hash": "7c8dfbe3575e55037b149b89f0d91077", "packages": [ { "name": "php-http/discovery", @@ -628,6 +628,57 @@ ], "time": "2025-07-17T20:45:56+00:00" }, + { + "name": "php-stubs/wordpress-stubs", + "version": "v6.8.2", + "source": { + "type": "git", + "url": "https://github.com/php-stubs/wordpress-stubs.git", + "reference": "9c8e22e437463197c1ec0d5eaa9ddd4a0eb6d7f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/9c8e22e437463197c1ec0d5eaa9ddd4a0eb6d7f8", + "reference": "9c8e22e437463197c1ec0d5eaa9ddd4a0eb6d7f8", + "shasum": "" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "5.6.1" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "nikic/php-parser": "^5.5", + "php": "^7.4 || ^8.0", + "php-stubs/generator": "^0.8.3", + "phpdocumentor/reflection-docblock": "^5.4.1", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9.5", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + }, + "suggest": { + "paragonie/sodium_compat": "Pure PHP implementation of libsodium", + "symfony/polyfill-php80": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress function and class declaration stubs for static analysis.", + "homepage": "https://github.com/php-stubs/wordpress-stubs", + "keywords": [ + "PHPStan", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/php-stubs/wordpress-stubs/issues", + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.8.2" + }, + "time": "2025-07-16T06:41:00+00:00" + }, { "name": "phpcompatibility/php-compatibility", "version": "9.3.5", @@ -1323,6 +1374,68 @@ ], "time": "2025-06-17T22:17:01+00:00" }, + { + "name": "szepeviktor/phpstan-wordpress", + "version": "v2.0.2", + "source": { + "type": "git", + "url": "https://github.com/szepeviktor/phpstan-wordpress.git", + "reference": "963887b04c21fe7ac78e61c1351f8b00fff9f8f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/963887b04c21fe7ac78e61c1351f8b00fff9f8f8", + "reference": "963887b04c21fe7ac78e61c1351f8b00fff9f8f8", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "php-stubs/wordpress-stubs": "^6.6.2", + "phpstan/phpstan": "^2.0" + }, + "require-dev": { + "composer/composer": "^2.1.14", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.0", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.0", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + }, + "suggest": { + "swissspidy/phpstan-no-private": "Detect usage of internal core functions, classes and methods" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "SzepeViktor\\PHPStan\\WordPress\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress extensions for PHPStan", + "keywords": [ + "PHPStan", + "code analyse", + "code analysis", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", + "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v2.0.2" + }, + "time": "2025-02-12T18:43:37+00:00" + }, { "name": "wp-coding-standards/wpcs", "version": "3.2.0", @@ -1392,14 +1505,14 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": ">=7.4", "ext-json": "*" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "7.4" }, diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 5dc1d9d..a393316 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,3 +1,6 @@ +includes: + - vendor/szepeviktor/phpstan-wordpress/extension.neon + parameters: level: max paths: From d0b9208e08abc479f22b3067f66c6a864e681618 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 9 Sep 2025 16:35:50 -0600 Subject: [PATCH 04/21] fix: corrects type issues --- includes/http/WP_AI_Client_Client_Adapter.php | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/includes/http/WP_AI_Client_Client_Adapter.php b/includes/http/WP_AI_Client_Client_Adapter.php index 20ed3b7..5f7c0bd 100644 --- a/includes/http/WP_AI_Client_Client_Adapter.php +++ b/includes/http/WP_AI_Client_Client_Adapter.php @@ -57,18 +57,19 @@ public function __construct( ResponseFactoryInterface $response_factory, StreamF * * @return ResponseInterface The PSR-7 response. * - * @throws ClientExceptionInterface If an error happens while processing the request. + * @throws \Exception If the WordPress HTTP request fails. */ public function sendRequest( RequestInterface $request ): ResponseInterface { $args = $this->prepare_wp_args( $request ); $url = (string) $request->getUri(); + /** Ignoring PHPStan for WordPress-specific array structure. @phpstan-ignore-next-line */ $response = \wp_remote_request( $url, $args ); if ( \is_wp_error( $response ) ) { // TODO: Update to use PHP AI Client exceptions. throw new \Exception( - $response->get_error_message(), + $response->get_error_message(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped $response->get_error_code() ? (int) $response->get_error_code() : 0 ); } @@ -81,7 +82,7 @@ public function sendRequest( RequestInterface $request ): ResponseInterface { * * @param RequestInterface $request The PSR-7 request. * - * @return array WordPress HTTP API arguments. + * @return array WordPress HTTP API arguments. */ private function prepare_wp_args( RequestInterface $request ): array { $args = array( @@ -108,7 +109,7 @@ private function prepare_wp_args( RequestInterface $request ): array { * * @param RequestInterface $request The PSR-7 request. * - * @return array Headers array for WordPress HTTP API. + * @return array Headers array for WordPress HTTP API. */ private function prepare_headers( RequestInterface $request ): array { $headers = array(); @@ -120,7 +121,7 @@ private function prepare_headers( RequestInterface $request ): array { } // WordPress expects headers as name => value pairs. - $headers[ $name ] = implode( ', ', $values ); + $headers[ (string) $name ] = implode( ', ', $values ); } return $headers; @@ -151,7 +152,7 @@ private function prepare_body( RequestInterface $request ): ?string { /** * Create PSR-7 response from WordPress HTTP response. * - * @param array $wp_response WordPress HTTP API response array. + * @param array{headers: \Traversable>|array>, body: string, response: array{code: int|string, message: string}} $wp_response WordPress HTTP API response array. * * @return ResponseInterface PSR-7 response. */ @@ -162,15 +163,21 @@ private function create_psr_response( array $wp_response ): ResponseInterface { $body = \wp_remote_retrieve_body( $wp_response ); // Create the PSR-7 response. - $response = $this->response_factory->createResponse( $status_code, $reason_phrase ); + $response = $this->response_factory->createResponse( (int) $status_code, $reason_phrase ); // Add headers to response. if ( $headers instanceof \WP_HTTP_Requests_Response ) { $headers = $headers->get_headers(); } - if ( is_array( $headers ) || $headers instanceof \ArrayAccess ) { + /** + * Headers from WordPress response. + * + * @var \Traversable>|array> $headers + */ + if ( is_array( $headers ) || $headers instanceof \Traversable ) { foreach ( $headers as $name => $value ) { + // PSR-7 expects string name and string|array value. $response = $response->withHeader( $name, $value ); } } @@ -183,4 +190,4 @@ private function create_psr_response( array $wp_response ): ResponseInterface { return $response; } -} \ No newline at end of file +} From d7ef812d3caf157224d81ca443426e6ed6c44fb7 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 9 Sep 2025 16:37:30 -0600 Subject: [PATCH 05/21] chore: cleans up phpstan file --- phpstan.neon.dist | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index a393316..61bfe22 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,8 +1,7 @@ -includes: - - vendor/szepeviktor/phpstan-wordpress/extension.neon - parameters: level: max paths: - includes treatPhpDocTypesAsCertain: false +includes: + - vendor/szepeviktor/phpstan-wordpress/extension.neon From f7ec38f823e179b59fd5ad8f9c6002baba95ec61 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 9 Sep 2025 17:06:29 -0600 Subject: [PATCH 06/21] feat: adds nyholm/psr7 package for psr17 factory --- composer.json | 3 +- composer.lock | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 35057f4..dbd79c8 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,8 @@ "require": { "php": ">=7.4", "ext-json": "*", - "wordpress/php-ai-client": "^0.1" + "wordpress/php-ai-client": "^0.1", + "nyholm/psr7": "^1.5" }, "require-dev": { "automattic/vipwpcs": "^3.0", diff --git a/composer.lock b/composer.lock index a96741a..405d1ef 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,86 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7c8dfbe3575e55037b149b89f0d91077", + "content-hash": "f66242c9a963b81dabfa08cf037d58d9", "packages": [ + { + "name": "nyholm/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-09-09T07:06:30+00:00" + }, { "name": "php-http/discovery", "version": "1.20.0", From 8010654558fb30d6dd427a6108389bff3167f39d Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 9 Sep 2025 17:06:51 -0600 Subject: [PATCH 07/21] feat: adds HTTPlug discovery strategy --- includes/http/WP_AI_Client_Client_Adapter.php | 5 +- .../http/WP_AI_Client_Discovery_Strategy.php | 74 +++++++++++++++++++ 2 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 includes/http/WP_AI_Client_Discovery_Strategy.php diff --git a/includes/http/WP_AI_Client_Client_Adapter.php b/includes/http/WP_AI_Client_Client_Adapter.php index 5f7c0bd..87d66a4 100644 --- a/includes/http/WP_AI_Client_Client_Adapter.php +++ b/includes/http/WP_AI_Client_Client_Adapter.php @@ -3,13 +3,12 @@ * WordPress AI Client HTTP Client Adapter * * @package WordPress\AI_Client - * @since 1.0.0 + * @since n.e.x.t */ namespace WordPress\AI_Client\HTTP; use Psr\Http\Client\ClientInterface; -use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseFactoryInterface; @@ -21,7 +20,7 @@ * This adapter allows WordPress HTTP functions to be used * as a PSR-18 compliant HTTP client. * - * @since 1.0.0 + * @since n.e.x.t */ class WP_AI_Client_Client_Adapter implements ClientInterface { diff --git a/includes/http/WP_AI_Client_Discovery_Strategy.php b/includes/http/WP_AI_Client_Discovery_Strategy.php new file mode 100644 index 0000000..43f085f --- /dev/null +++ b/includes/http/WP_AI_Client_Discovery_Strategy.php @@ -0,0 +1,74 @@ +> + */ + public static function getCandidates( $type ) { + if ( ClientInterface::class === $type ) { + return array( + array( + 'class' => array( __CLASS__, 'createWordPressClient' ), + 'condition' => array( + WP_AI_Client_Client_Adapter::class, + Psr17Factory::class, + ), + ), + ); + } + + return array(); + } + + /** + * Create an instance of the WordPress HTTP client. + * + * @return WP_AI_Client_Client_Adapter + */ + public static function createWordPressClient() { + $psr17_factory = new Psr17Factory(); + return new WP_AI_Client_Client_Adapter( + $psr17_factory, // Response factory. + $psr17_factory // Stream factory. + ); + } +} From 4e0247fff1377c6da317583880c1cca9e2ee4978 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 9 Sep 2025 17:13:33 -0600 Subject: [PATCH 08/21] feat: adds PSR17 discoverability --- .../http/WP_AI_Client_Discovery_Strategy.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/includes/http/WP_AI_Client_Discovery_Strategy.php b/includes/http/WP_AI_Client_Discovery_Strategy.php index 43f085f..6815f66 100644 --- a/includes/http/WP_AI_Client_Discovery_Strategy.php +++ b/includes/http/WP_AI_Client_Discovery_Strategy.php @@ -44,6 +44,7 @@ public static function init() { * @return array> */ public static function getCandidates( $type ) { + // PSR-18 HTTP Client. if ( ClientInterface::class === $type ) { return array( array( @@ -56,6 +57,25 @@ public static function getCandidates( $type ) { ); } + // PSR-17 factories - Nyholm's Psr17Factory implements all of them. + $psr17_factories = array( + 'Psr\Http\Message\RequestFactoryInterface', + 'Psr\Http\Message\ResponseFactoryInterface', + 'Psr\Http\Message\ServerRequestFactoryInterface', + 'Psr\Http\Message\StreamFactoryInterface', + 'Psr\Http\Message\UploadedFileFactoryInterface', + 'Psr\Http\Message\UriFactoryInterface', + ); + + if ( in_array( $type, $psr17_factories, true ) ) { + return array( + array( + 'class' => Psr17Factory::class, + 'condition' => Psr17Factory::class, + ), + ); + } + return array(); } From 3998eb7ed4eabedd9616b050c44b6d8e08eb5d7a Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Mon, 22 Sep 2025 10:51:18 -0600 Subject: [PATCH 09/21] refactor: renames class --- includes/http/WP_AI_Client_Discovery_Strategy.php | 6 +++--- ..._Client_Client_Adapter.php => WordPress_HTTP_Client.php} | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename includes/http/{WP_AI_Client_Client_Adapter.php => WordPress_HTTP_Client.php} (98%) diff --git a/includes/http/WP_AI_Client_Discovery_Strategy.php b/includes/http/WP_AI_Client_Discovery_Strategy.php index 6815f66..91b5618 100644 --- a/includes/http/WP_AI_Client_Discovery_Strategy.php +++ b/includes/http/WP_AI_Client_Discovery_Strategy.php @@ -50,7 +50,7 @@ public static function getCandidates( $type ) { array( 'class' => array( __CLASS__, 'createWordPressClient' ), 'condition' => array( - WP_AI_Client_Client_Adapter::class, + WordPress_HTTP_Client::class, Psr17Factory::class, ), ), @@ -82,11 +82,11 @@ public static function getCandidates( $type ) { /** * Create an instance of the WordPress HTTP client. * - * @return WP_AI_Client_Client_Adapter + * @return WordPress_HTTP_Client */ public static function createWordPressClient() { $psr17_factory = new Psr17Factory(); - return new WP_AI_Client_Client_Adapter( + return new WordPress_HTTP_Client( $psr17_factory, // Response factory. $psr17_factory // Stream factory. ); diff --git a/includes/http/WP_AI_Client_Client_Adapter.php b/includes/http/WordPress_HTTP_Client.php similarity index 98% rename from includes/http/WP_AI_Client_Client_Adapter.php rename to includes/http/WordPress_HTTP_Client.php index 87d66a4..1ce9a53 100644 --- a/includes/http/WP_AI_Client_Client_Adapter.php +++ b/includes/http/WordPress_HTTP_Client.php @@ -22,7 +22,7 @@ * * @since n.e.x.t */ -class WP_AI_Client_Client_Adapter implements ClientInterface { +class WordPress_HTTP_Client implements ClientInterface { /** * Response factory instance. From 581de9eec18f839053cfce771797057c45213e05 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Mon, 22 Sep 2025 10:53:30 -0600 Subject: [PATCH 10/21] refactor: removes stream args --- includes/http/WordPress_HTTP_Client.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/includes/http/WordPress_HTTP_Client.php b/includes/http/WordPress_HTTP_Client.php index 1ce9a53..c94694a 100644 --- a/includes/http/WordPress_HTTP_Client.php +++ b/includes/http/WordPress_HTTP_Client.php @@ -94,12 +94,6 @@ private function prepare_wp_args( RequestInterface $request ): array { 'blocking' => true, ); - // Handle streaming requests if needed. - if ( $request->hasHeader( 'X-Stream' ) ) { - $args['stream'] = true; - $args['filename'] = $request->getHeaderLine( 'X-Stream-Filename' ); - } - return $args; } From 5c66fded99c6b7237dbca4887e5d2c10e38fb586 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 21 Oct 2025 16:18:00 -0600 Subject: [PATCH 11/21] feat: updates to PHP AI Client 0.2.0 --- composer.json | 2 +- composer.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index dbd79c8..d33b984 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ "require": { "php": ">=7.4", "ext-json": "*", - "wordpress/php-ai-client": "^0.1", + "wordpress/php-ai-client": "^0.2", "nyholm/psr7": "^1.5" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 405d1ef..4d64abd 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f66242c9a963b81dabfa08cf037d58d9", + "content-hash": "14bf56bd9ce31980677d9d62627e2b4a", "packages": [ { "name": "nyholm/psr7", @@ -489,16 +489,16 @@ }, { "name": "wordpress/php-ai-client", - "version": "0.1.0", + "version": "0.2.0", "source": { "type": "git", "url": "https://github.com/WordPress/php-ai-client.git", - "reference": "9ec56e70e692791493a3eaff1b69f25f4daeded7" + "reference": "81a104a9bc5f887e3fbecea6e0d9cd8eab3be0b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress/php-ai-client/zipball/9ec56e70e692791493a3eaff1b69f25f4daeded7", - "reference": "9ec56e70e692791493a3eaff1b69f25f4daeded7", + "url": "https://api.github.com/repos/WordPress/php-ai-client/zipball/81a104a9bc5f887e3fbecea6e0d9cd8eab3be0b2", + "reference": "81a104a9bc5f887e3fbecea6e0d9cd8eab3be0b2", "shasum": "" }, "require": { @@ -552,7 +552,7 @@ "issues": "https://github.com/WordPress/php-ai-client/issues", "source": "https://github.com/WordPress/php-ai-client" }, - "time": "2025-08-29T22:46:54+00:00" + "time": "2025-10-21T00:05:14+00:00" } ], "packages-dev": [ From 4e80b8974eacdd93e4aa3c4cc41d0d31f03c2a06 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 21 Oct 2025 16:19:17 -0600 Subject: [PATCH 12/21] feat: implements ClientWithOptionsInterface --- includes/http/WordPress_HTTP_Client.php | 54 ++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/includes/http/WordPress_HTTP_Client.php b/includes/http/WordPress_HTTP_Client.php index c94694a..c2dba44 100644 --- a/includes/http/WordPress_HTTP_Client.php +++ b/includes/http/WordPress_HTTP_Client.php @@ -13,6 +13,8 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\StreamFactoryInterface; +use WordPress\AiClient\Providers\Http\Contracts\ClientWithOptionsInterface; +use WordPress\AiClient\Providers\Http\DTO\RequestOptions; /** * PSR-18 HTTP Client adapter using WordPress HTTP API @@ -22,7 +24,7 @@ * * @since n.e.x.t */ -class WordPress_HTTP_Client implements ClientInterface { +class WordPress_HTTP_Client implements ClientInterface, ClientWithOptionsInterface { /** * Response factory instance. @@ -77,23 +79,65 @@ public function sendRequest( RequestInterface $request ): ResponseInterface { } /** - * Prepare WordPress HTTP API arguments from PSR-7 request. + * Sends a PSR-7 request with transport options and returns a PSR-7 response. + * + * @since n.e.x.t * * @param RequestInterface $request The PSR-7 request. + * @param RequestOptions $options Transport options for the request. + * + * @return ResponseInterface The PSR-7 response. + * + * @throws \Exception If the WordPress HTTP request fails. + */ + public function sendRequestWithOptions( RequestInterface $request, RequestOptions $options ): ResponseInterface { + $args = $this->prepare_wp_args( $request, $options ); + $url = (string) $request->getUri(); + + /** Ignoring PHPStan for WordPress-specific array structure. @phpstan-ignore-next-line */ + $response = \wp_remote_request( $url, $args ); + + if ( \is_wp_error( $response ) ) { + // TODO: Update to use PHP AI Client exceptions. + throw new \Exception( + $response->get_error_message(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + $response->get_error_code() ? (int) $response->get_error_code() : 0 + ); + } + + return $this->create_psr_response( $response ); + } + + /** + * Prepare WordPress HTTP API arguments from PSR-7 request. + * + * @param RequestInterface $request The PSR-7 request. + * @param RequestOptions|null $options Optional transport options for the request. * * @return array WordPress HTTP API arguments. */ - private function prepare_wp_args( RequestInterface $request ): array { + private function prepare_wp_args( RequestInterface $request, ?RequestOptions $options = null ): array { $args = array( 'method' => $request->getMethod(), 'headers' => $this->prepare_headers( $request ), 'body' => $this->prepare_body( $request ), - 'timeout' => 30, - 'redirection' => 5, 'httpversion' => $request->getProtocolVersion(), 'blocking' => true, ); + // Apply options if provided. + if ( null !== $options ) { + // Set timeout if specified. + if ( null !== $options->getTimeout() ) { + $args['timeout'] = $options->getTimeout(); + } + + // Set redirection if specified. + if ( null !== $options->getMaxRedirects() ) { + $args['redirection'] = $options->getMaxRedirects(); + } + } + return $args; } From de7de24d41b71949f3c7a32e2db17c819cfa2b1d Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Tue, 21 Oct 2025 16:23:09 -0600 Subject: [PATCH 13/21] refactor: switches to using NetworkException --- includes/http/WordPress_HTTP_Client.php | 27 +++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/includes/http/WordPress_HTTP_Client.php b/includes/http/WordPress_HTTP_Client.php index c2dba44..1f43be4 100644 --- a/includes/http/WordPress_HTTP_Client.php +++ b/includes/http/WordPress_HTTP_Client.php @@ -15,6 +15,7 @@ use Psr\Http\Message\StreamFactoryInterface; use WordPress\AiClient\Providers\Http\Contracts\ClientWithOptionsInterface; use WordPress\AiClient\Providers\Http\DTO\RequestOptions; +use WordPress\AiClient\Providers\Http\Exception\NetworkException; /** * PSR-18 HTTP Client adapter using WordPress HTTP API @@ -58,7 +59,7 @@ public function __construct( ResponseFactoryInterface $response_factory, StreamF * * @return ResponseInterface The PSR-7 response. * - * @throws \Exception If the WordPress HTTP request fails. + * @throws NetworkException If the WordPress HTTP request fails. */ public function sendRequest( RequestInterface $request ): ResponseInterface { $args = $this->prepare_wp_args( $request ); @@ -68,9 +69,14 @@ public function sendRequest( RequestInterface $request ): ResponseInterface { $response = \wp_remote_request( $url, $args ); if ( \is_wp_error( $response ) ) { - // TODO: Update to use PHP AI Client exceptions. - throw new \Exception( - $response->get_error_message(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + $message = sprintf( + 'Network error occurred while sending request to %s: %s', + $url, + $response->get_error_message() + ); + + throw new NetworkException( + $message, // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped $response->get_error_code() ? (int) $response->get_error_code() : 0 ); } @@ -88,7 +94,7 @@ public function sendRequest( RequestInterface $request ): ResponseInterface { * * @return ResponseInterface The PSR-7 response. * - * @throws \Exception If the WordPress HTTP request fails. + * @throws NetworkException If the WordPress HTTP request fails. */ public function sendRequestWithOptions( RequestInterface $request, RequestOptions $options ): ResponseInterface { $args = $this->prepare_wp_args( $request, $options ); @@ -98,9 +104,14 @@ public function sendRequestWithOptions( RequestInterface $request, RequestOption $response = \wp_remote_request( $url, $args ); if ( \is_wp_error( $response ) ) { - // TODO: Update to use PHP AI Client exceptions. - throw new \Exception( - $response->get_error_message(), // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped + $message = sprintf( + 'Network error occurred while sending request to %s: %s', + $url, + $response->get_error_message() + ); + + throw new NetworkException( + $message, // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped $response->get_error_code() ? (int) $response->get_error_code() : 0 ); } From 7943ed6e8ebdeeee49a180d45008a4c229312368 Mon Sep 17 00:00:00 2001 From: Jason Adams Date: Wed, 5 Nov 2025 15:06:39 -0500 Subject: [PATCH 14/21] refactor: cleans up unnecessary conditions code --- includes/http/WP_AI_Client_Discovery_Strategy.php | 11 +++-------- includes/http/WordPress_HTTP_Client.php | 8 +++----- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/includes/http/WP_AI_Client_Discovery_Strategy.php b/includes/http/WP_AI_Client_Discovery_Strategy.php index 91b5618..c8e6571 100644 --- a/includes/http/WP_AI_Client_Discovery_Strategy.php +++ b/includes/http/WP_AI_Client_Discovery_Strategy.php @@ -48,11 +48,7 @@ public static function getCandidates( $type ) { if ( ClientInterface::class === $type ) { return array( array( - 'class' => array( __CLASS__, 'createWordPressClient' ), - 'condition' => array( - WordPress_HTTP_Client::class, - Psr17Factory::class, - ), + 'class' => self::createWordPressClient(), ), ); } @@ -70,8 +66,7 @@ public static function getCandidates( $type ) { if ( in_array( $type, $psr17_factories, true ) ) { return array( array( - 'class' => Psr17Factory::class, - 'condition' => Psr17Factory::class, + 'class' => Psr17Factory::class, ), ); } @@ -84,7 +79,7 @@ public static function getCandidates( $type ) { * * @return WordPress_HTTP_Client */ - public static function createWordPressClient() { + private static function createWordPressClient() { $psr17_factory = new Psr17Factory(); return new WordPress_HTTP_Client( $psr17_factory, // Response factory. diff --git a/includes/http/WordPress_HTTP_Client.php b/includes/http/WordPress_HTTP_Client.php index 1f43be4..d12b318 100644 --- a/includes/http/WordPress_HTTP_Client.php +++ b/includes/http/WordPress_HTTP_Client.php @@ -70,15 +70,13 @@ public function sendRequest( RequestInterface $request ): ResponseInterface { if ( \is_wp_error( $response ) ) { $message = sprintf( - 'Network error occurred while sending request to %s: %s', + 'Network error occurred while sending %s request to %s: %s', + $request->getMethod(), $url, $response->get_error_message() ); - throw new NetworkException( - $message, // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped - $response->get_error_code() ? (int) $response->get_error_code() : 0 - ); + throw new NetworkException( $message ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped } return $this->create_psr_response( $response ); From 990111a4a7a1067dadeea2d43824eded06bca23a Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 7 Nov 2025 08:37:03 -0800 Subject: [PATCH 15/21] Use consistent YAML formatting in PHPStan config. --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index b5df0fc..c890705 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,6 +13,6 @@ insert_final_newline = true trim_trailing_whitespace = true indent_style = tab -[*.{yml,yaml}] +[*.{yml,yaml,neon.dist}] indent_style = space indent_size = 2 From acd0e8b66e37ffa82a818e792ee4555c7713de8b Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 7 Nov 2025 08:49:59 -0800 Subject: [PATCH 16/21] Fix PHPStan errors after updates. --- .../API_Credentials_Manager.php | 30 ++++++++++++++++--- .../API_Credentials_Settings_Screen.php | 8 ++++- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/includes/API_Credentials/API_Credentials_Manager.php b/includes/API_Credentials/API_Credentials_Manager.php index bfb8ad1..1b072bd 100644 --- a/includes/API_Credentials/API_Credentials_Manager.php +++ b/includes/API_Credentials/API_Credentials_Manager.php @@ -76,6 +76,8 @@ function () { * is registered in. * * @since n.e.x.t + * + * @throws RuntimeException If the collected provider metadata is in an invalid format. */ private function collect_providers(): void { /** @@ -95,13 +97,22 @@ private function collect_providers(): void { foreach ( $provider_ids as $provider_id ) { // If the provider was already found via another client class, just add this client class name to the list. if ( isset( $wp_ai_client_providers_metadata[ $provider_id ] ) ) { + if ( ! is_array( $wp_ai_client_providers_metadata[ $provider_id ]['ai_client_classnames'] ) ) { + throw new RuntimeException( 'Invalid format for collected provider AI client class names.' ); + } $wp_ai_client_providers_metadata[ $provider_id ]['ai_client_classnames'][ AiClient::class ] = true; continue; } // Otherwise, get the provider metadata and add it to the global. $provider_class_name = $registry->getProviderClassName( $provider_id ); - $provider_metadata = $provider_class_name::metadata(); + + /** + * The provider metadata. + * + * @var ProviderMetadata + */ + $provider_metadata = $provider_class_name::metadata(); $wp_ai_client_providers_metadata[ $provider_id ] = array_merge( $provider_metadata->toArray(), @@ -191,6 +202,10 @@ private function register_settings(): void { $credentials = array_intersect_key( $credentials, $providers_metadata_keyed_by_ids ); foreach ( $credentials as $provider_id => $api_key ) { + if ( ! is_string( $api_key ) ) { + unset( $credentials[ $provider_id ] ); + continue; + } $credentials[ $provider_id ] = sanitize_text_field( $api_key ); } return $credentials; @@ -218,7 +233,7 @@ private function pass_credentials_to_client(): void { // Set available API keys for all registered providers. foreach ( $credentials as $provider_id => $api_key ) { - if ( '' === $api_key ) { + if ( ! is_string( $api_key ) || '' === $api_key ) { continue; } @@ -248,8 +263,15 @@ private function add_admin_screen(): void { // Bail if the screen was already added (e.g. by another instance of this package). if ( - isset( $_wp_submenu_nopriv[ $parent_slug ][ $screen_slug ] ) || - isset( $_parent_pages[ $screen_slug ] ) + ( + is_array( $_wp_submenu_nopriv ) && + is_array( $_wp_submenu_nopriv[ $parent_slug ] ) && + isset( $_wp_submenu_nopriv[ $parent_slug ][ $screen_slug ] ) + ) || + ( + is_array( $_parent_pages ) && + isset( $_parent_pages[ $screen_slug ] ) + ) ) { return; } diff --git a/includes/API_Credentials/API_Credentials_Settings_Screen.php b/includes/API_Credentials/API_Credentials_Settings_Screen.php index 4552151..47aa536 100644 --- a/includes/API_Credentials/API_Credentials_Settings_Screen.php +++ b/includes/API_Credentials/API_Credentials_Settings_Screen.php @@ -201,7 +201,13 @@ public function render_field( array $args ): void { $option = get_option( $parts[0] ); $subkey = trim( $parts[1], ']' ); if ( is_array( $option ) && isset( $option[ $subkey ] ) ) { - $value = $option[ $subkey ]; + if ( is_string( $option[ $subkey ] ) ) { + $value = $option[ $subkey ]; + } elseif ( is_numeric( $option[ $subkey ] ) ) { + $value = (string) $option[ $subkey ]; + } else { + $value = ''; + } } else { $value = ''; } From a68e34cd863c6fc79150c5dd8a72736be76de261 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 7 Nov 2025 08:55:49 -0800 Subject: [PATCH 17/21] Temp folder rename to fix incorrect casing. --- includes/{http => httptemp}/WP_AI_Client_Discovery_Strategy.php | 0 includes/{http => httptemp}/WordPress_HTTP_Client.php | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename includes/{http => httptemp}/WP_AI_Client_Discovery_Strategy.php (100%) rename includes/{http => httptemp}/WordPress_HTTP_Client.php (100%) diff --git a/includes/http/WP_AI_Client_Discovery_Strategy.php b/includes/httptemp/WP_AI_Client_Discovery_Strategy.php similarity index 100% rename from includes/http/WP_AI_Client_Discovery_Strategy.php rename to includes/httptemp/WP_AI_Client_Discovery_Strategy.php diff --git a/includes/http/WordPress_HTTP_Client.php b/includes/httptemp/WordPress_HTTP_Client.php similarity index 100% rename from includes/http/WordPress_HTTP_Client.php rename to includes/httptemp/WordPress_HTTP_Client.php From bf5410178d910f100d9d99d7c048a071786f17a2 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 7 Nov 2025 08:56:30 -0800 Subject: [PATCH 18/21] Fix folder name. --- includes/{httptemp => HTTP}/WP_AI_Client_Discovery_Strategy.php | 0 includes/{httptemp => HTTP}/WordPress_HTTP_Client.php | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename includes/{httptemp => HTTP}/WP_AI_Client_Discovery_Strategy.php (100%) rename includes/{httptemp => HTTP}/WordPress_HTTP_Client.php (100%) diff --git a/includes/httptemp/WP_AI_Client_Discovery_Strategy.php b/includes/HTTP/WP_AI_Client_Discovery_Strategy.php similarity index 100% rename from includes/httptemp/WP_AI_Client_Discovery_Strategy.php rename to includes/HTTP/WP_AI_Client_Discovery_Strategy.php diff --git a/includes/httptemp/WordPress_HTTP_Client.php b/includes/HTTP/WordPress_HTTP_Client.php similarity index 100% rename from includes/httptemp/WordPress_HTTP_Client.php rename to includes/HTTP/WordPress_HTTP_Client.php From 167aeb6c7f641541766d0dc131b257dea26264ec Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 7 Nov 2025 09:02:55 -0800 Subject: [PATCH 19/21] Fix PHP warning. --- includes/API_Credentials/API_Credentials_Manager.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/API_Credentials/API_Credentials_Manager.php b/includes/API_Credentials/API_Credentials_Manager.php index 1b072bd..4ca97ba 100644 --- a/includes/API_Credentials/API_Credentials_Manager.php +++ b/includes/API_Credentials/API_Credentials_Manager.php @@ -265,6 +265,7 @@ private function add_admin_screen(): void { if ( ( is_array( $_wp_submenu_nopriv ) && + isset( $_wp_submenu_nopriv[ $parent_slug ] ) && is_array( $_wp_submenu_nopriv[ $parent_slug ] ) && isset( $_wp_submenu_nopriv[ $parent_slug ][ $screen_slug ] ) ) || From c50d84b15d4bd30f9873d215cc1990ca948b022e Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 7 Nov 2025 09:03:26 -0800 Subject: [PATCH 20/21] Fix bug in HTTP client discovery implementation. --- includes/HTTP/WP_AI_Client_Discovery_Strategy.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/includes/HTTP/WP_AI_Client_Discovery_Strategy.php b/includes/HTTP/WP_AI_Client_Discovery_Strategy.php index c8e6571..ca03e9b 100644 --- a/includes/HTTP/WP_AI_Client_Discovery_Strategy.php +++ b/includes/HTTP/WP_AI_Client_Discovery_Strategy.php @@ -48,7 +48,9 @@ public static function getCandidates( $type ) { if ( ClientInterface::class === $type ) { return array( array( - 'class' => self::createWordPressClient(), + 'class' => static function () { + return self::createWordPressClient(); + }, ), ); } From a8aaf959781116a29cc9430ff60ebc81c0109569 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Fri, 7 Nov 2025 09:04:06 -0800 Subject: [PATCH 21/21] Wire up HTTP client discovery implementation. --- plugin.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugin.php b/plugin.php index c2cf866..be37c5b 100644 --- a/plugin.php +++ b/plugin.php @@ -24,6 +24,10 @@ add_action( 'init', static function () { + // Wire up the WordPress HTTP client with the PHP AI Client SDK. + WordPress\AI_Client\HTTP\WP_AI_Client_Discovery_Strategy::init(); + + // Initialize the API credentials manager and settings screen. $api_credentials_manager = new WordPress\AI_Client\API_Credentials\API_Credentials_Manager(); $api_credentials_manager->initialize(); }