+ *
+ * @author Nicolas Grekas
+ */
+class Psr17Factory implements RequestFactoryInterface, ResponseFactoryInterface, ServerRequestFactoryInterface, StreamFactoryInterface, UploadedFileFactoryInterface, UriFactoryInterface
+{
+ private $requestFactory;
+ private $responseFactory;
+ private $serverRequestFactory;
+ private $streamFactory;
+ private $uploadedFileFactory;
+ private $uriFactory;
+ public function __construct(?RequestFactoryInterface $requestFactory = null, ?ResponseFactoryInterface $responseFactory = null, ?ServerRequestFactoryInterface $serverRequestFactory = null, ?StreamFactoryInterface $streamFactory = null, ?UploadedFileFactoryInterface $uploadedFileFactory = null, ?UriFactoryInterface $uriFactory = null)
+ {
+ $this->requestFactory = $requestFactory;
+ $this->responseFactory = $responseFactory;
+ $this->serverRequestFactory = $serverRequestFactory;
+ $this->streamFactory = $streamFactory;
+ $this->uploadedFileFactory = $uploadedFileFactory;
+ $this->uriFactory = $uriFactory;
+ $this->setFactory($requestFactory);
+ $this->setFactory($responseFactory);
+ $this->setFactory($serverRequestFactory);
+ $this->setFactory($streamFactory);
+ $this->setFactory($uploadedFileFactory);
+ $this->setFactory($uriFactory);
+ }
+ /**
+ * @param UriInterface|string $uri
+ */
+ public function createRequest(string $method, $uri): RequestInterface
+ {
+ $factory = $this->requestFactory ?? $this->setFactory(Psr17FactoryDiscovery::findRequestFactory());
+ return $factory->createRequest(...\func_get_args());
+ }
+ public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface
+ {
+ $factory = $this->responseFactory ?? $this->setFactory(Psr17FactoryDiscovery::findResponseFactory());
+ return $factory->createResponse(...\func_get_args());
+ }
+ /**
+ * @param UriInterface|string $uri
+ */
+ public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
+ {
+ $factory = $this->serverRequestFactory ?? $this->setFactory(Psr17FactoryDiscovery::findServerRequestFactory());
+ return $factory->createServerRequest(...\func_get_args());
+ }
+ public function createServerRequestFromGlobals(?array $server = null, ?array $get = null, ?array $post = null, ?array $cookie = null, ?array $files = null, ?StreamInterface $body = null): ServerRequestInterface
+ {
+ $server = $server ?? $_SERVER;
+ $request = $this->createServerRequest($server['REQUEST_METHOD'] ?? 'GET', $this->createUriFromGlobals($server), $server);
+ return $this->buildServerRequestFromGlobals($request, $server, $files ?? $_FILES)->withQueryParams($get ?? $_GET)->withParsedBody($post ?? $_POST)->withCookieParams($cookie ?? $_COOKIE)->withBody($body ?? $this->createStreamFromFile('php://input', 'r+'));
+ }
+ public function createStream(string $content = ''): StreamInterface
+ {
+ $factory = $this->streamFactory ?? $this->setFactory(Psr17FactoryDiscovery::findStreamFactory());
+ return $factory->createStream($content);
+ }
+ public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface
+ {
+ $factory = $this->streamFactory ?? $this->setFactory(Psr17FactoryDiscovery::findStreamFactory());
+ return $factory->createStreamFromFile($filename, $mode);
+ }
+ /**
+ * @param resource $resource
+ */
+ public function createStreamFromResource($resource): StreamInterface
+ {
+ $factory = $this->streamFactory ?? $this->setFactory(Psr17FactoryDiscovery::findStreamFactory());
+ return $factory->createStreamFromResource($resource);
+ }
+ public function createUploadedFile(StreamInterface $stream, ?int $size = null, int $error = \UPLOAD_ERR_OK, ?string $clientFilename = null, ?string $clientMediaType = null): UploadedFileInterface
+ {
+ $factory = $this->uploadedFileFactory ?? $this->setFactory(Psr17FactoryDiscovery::findUploadedFileFactory());
+ return $factory->createUploadedFile(...\func_get_args());
+ }
+ public function createUri(string $uri = ''): UriInterface
+ {
+ $factory = $this->uriFactory ?? $this->setFactory(Psr17FactoryDiscovery::findUriFactory());
+ return $factory->createUri(...\func_get_args());
+ }
+ public function createUriFromGlobals(?array $server = null): UriInterface
+ {
+ return $this->buildUriFromGlobals($this->createUri(''), $server ?? $_SERVER);
+ }
+ private function setFactory($factory)
+ {
+ if (!$this->requestFactory && $factory instanceof RequestFactoryInterface) {
+ $this->requestFactory = $factory;
+ }
+ if (!$this->responseFactory && $factory instanceof ResponseFactoryInterface) {
+ $this->responseFactory = $factory;
+ }
+ if (!$this->serverRequestFactory && $factory instanceof ServerRequestFactoryInterface) {
+ $this->serverRequestFactory = $factory;
+ }
+ if (!$this->streamFactory && $factory instanceof StreamFactoryInterface) {
+ $this->streamFactory = $factory;
+ }
+ if (!$this->uploadedFileFactory && $factory instanceof UploadedFileFactoryInterface) {
+ $this->uploadedFileFactory = $factory;
+ }
+ if (!$this->uriFactory && $factory instanceof UriFactoryInterface) {
+ $this->uriFactory = $factory;
+ }
+ return $factory;
+ }
+ private function buildServerRequestFromGlobals(ServerRequestInterface $request, array $server, array $files): ServerRequestInterface
+ {
+ $request = $request->withProtocolVersion(isset($server['SERVER_PROTOCOL']) ? str_replace('HTTP/', '', $server['SERVER_PROTOCOL']) : '1.1')->withUploadedFiles($this->normalizeFiles($files));
+ $headers = [];
+ foreach ($server as $k => $v) {
+ if (0 === strpos($k, 'HTTP_')) {
+ $k = substr($k, 5);
+ } elseif (!\in_array($k, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'], \true)) {
+ continue;
+ }
+ $k = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $k))));
+ $headers[$k] = $v;
+ }
+ if (!isset($headers['Authorization'])) {
+ if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
+ $headers['Authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
+ } elseif (isset($_SERVER['PHP_AUTH_USER'])) {
+ $headers['Authorization'] = 'Basic ' . base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . ($_SERVER['PHP_AUTH_PW'] ?? ''));
+ } elseif (isset($_SERVER['PHP_AUTH_DIGEST'])) {
+ $headers['Authorization'] = $_SERVER['PHP_AUTH_DIGEST'];
+ }
+ }
+ foreach ($headers as $k => $v) {
+ try {
+ $request = $request->withHeader($k, $v);
+ } catch (\InvalidArgumentException $e) {
+ // ignore invalid headers
+ }
+ }
+ return $request;
+ }
+ private function buildUriFromGlobals(UriInterface $uri, array $server): UriInterface
+ {
+ $uri = $uri->withScheme(!empty($server['HTTPS']) && 'off' !== strtolower($server['HTTPS']) ? 'https' : 'http');
+ $hasPort = \false;
+ if (isset($server['HTTP_HOST'])) {
+ $parts = parse_url('http://' . $server['HTTP_HOST']);
+ $uri = $uri->withHost($parts['host'] ?? 'localhost');
+ if ($parts['port'] ?? \false) {
+ $hasPort = \true;
+ $uri = $uri->withPort($parts['port']);
+ }
+ } else {
+ $uri = $uri->withHost($server['SERVER_NAME'] ?? $server['SERVER_ADDR'] ?? 'localhost');
+ }
+ if (!$hasPort && isset($server['SERVER_PORT'])) {
+ $uri = $uri->withPort($server['SERVER_PORT']);
+ }
+ $hasQuery = \false;
+ if (isset($server['REQUEST_URI'])) {
+ $requestUriParts = explode('?', $server['REQUEST_URI'], 2);
+ $uri = $uri->withPath($requestUriParts[0]);
+ if (isset($requestUriParts[1])) {
+ $hasQuery = \true;
+ $uri = $uri->withQuery($requestUriParts[1]);
+ }
+ }
+ if (!$hasQuery && isset($server['QUERY_STRING'])) {
+ $uri = $uri->withQuery($server['QUERY_STRING']);
+ }
+ return $uri;
+ }
+ private function normalizeFiles(array $files): array
+ {
+ foreach ($files as $k => $v) {
+ if ($v instanceof UploadedFileInterface) {
+ continue;
+ }
+ if (!\is_array($v)) {
+ unset($files[$k]);
+ } elseif (!isset($v['tmp_name'])) {
+ $files[$k] = $this->normalizeFiles($v);
+ } else {
+ $files[$k] = $this->createUploadedFileFromSpec($v);
+ }
+ }
+ return $files;
+ }
+ /**
+ * Create and return an UploadedFile instance from a $_FILES specification.
+ *
+ * @param array $value $_FILES struct
+ *
+ * @return UploadedFileInterface|UploadedFileInterface[]
+ */
+ private function createUploadedFileFromSpec(array $value)
+ {
+ if (!is_array($tmpName = $value['tmp_name'])) {
+ $file = is_file($tmpName) ? $this->createStreamFromFile($tmpName, 'r') : $this->createStream();
+ return $this->createUploadedFile($file, $value['size'], $value['error'], $value['name'], $value['type']);
+ }
+ foreach ($tmpName as $k => $v) {
+ $tmpName[$k] = $this->createUploadedFileFromSpec(['tmp_name' => $v, 'size' => $value['size'][$k] ?? null, 'error' => $value['error'][$k] ?? null, 'name' => $value['name'][$k] ?? null, 'type' => $value['type'][$k] ?? null]);
+ }
+ return $tmpName;
+ }
+}
diff --git a/src/wp-includes/php-ai-client/third-party/Http/Discovery/Psr17FactoryDiscovery.php b/src/wp-includes/php-ai-client/third-party/Http/Discovery/Psr17FactoryDiscovery.php
new file mode 100644
index 0000000000000..d9e5f9cd42f27
--- /dev/null
+++ b/src/wp-includes/php-ai-client/third-party/Http/Discovery/Psr17FactoryDiscovery.php
@@ -0,0 +1,119 @@
+
+ */
+final class Psr17FactoryDiscovery extends ClassDiscovery
+{
+ private static function createException($type, Exception $e)
+ {
+ return new RealNotFoundException('No PSR-17 ' . $type . ' found. Install a package from this list: https://packagist.org/providers/psr/http-factory-implementation', 0, $e);
+ }
+ /**
+ * @return RequestFactoryInterface
+ *
+ * @throws RealNotFoundException
+ */
+ public static function findRequestFactory()
+ {
+ try {
+ $messageFactory = static::findOneByType(RequestFactoryInterface::class);
+ } catch (DiscoveryFailedException $e) {
+ throw self::createException('request factory', $e);
+ }
+ return static::instantiateClass($messageFactory);
+ }
+ /**
+ * @return ResponseFactoryInterface
+ *
+ * @throws RealNotFoundException
+ */
+ public static function findResponseFactory()
+ {
+ try {
+ $messageFactory = static::findOneByType(ResponseFactoryInterface::class);
+ } catch (DiscoveryFailedException $e) {
+ throw self::createException('response factory', $e);
+ }
+ return static::instantiateClass($messageFactory);
+ }
+ /**
+ * @return ServerRequestFactoryInterface
+ *
+ * @throws RealNotFoundException
+ */
+ public static function findServerRequestFactory()
+ {
+ try {
+ $messageFactory = static::findOneByType(ServerRequestFactoryInterface::class);
+ } catch (DiscoveryFailedException $e) {
+ throw self::createException('server request factory', $e);
+ }
+ return static::instantiateClass($messageFactory);
+ }
+ /**
+ * @return StreamFactoryInterface
+ *
+ * @throws RealNotFoundException
+ */
+ public static function findStreamFactory()
+ {
+ try {
+ $messageFactory = static::findOneByType(StreamFactoryInterface::class);
+ } catch (DiscoveryFailedException $e) {
+ throw self::createException('stream factory', $e);
+ }
+ return static::instantiateClass($messageFactory);
+ }
+ /**
+ * @return UploadedFileFactoryInterface
+ *
+ * @throws RealNotFoundException
+ */
+ public static function findUploadedFileFactory()
+ {
+ try {
+ $messageFactory = static::findOneByType(UploadedFileFactoryInterface::class);
+ } catch (DiscoveryFailedException $e) {
+ throw self::createException('uploaded file factory', $e);
+ }
+ return static::instantiateClass($messageFactory);
+ }
+ /**
+ * @return UriFactoryInterface
+ *
+ * @throws RealNotFoundException
+ */
+ public static function findUriFactory()
+ {
+ try {
+ $messageFactory = static::findOneByType(UriFactoryInterface::class);
+ } catch (DiscoveryFailedException $e) {
+ throw self::createException('url factory', $e);
+ }
+ return static::instantiateClass($messageFactory);
+ }
+ /**
+ * @return UriFactoryInterface
+ *
+ * @throws RealNotFoundException
+ *
+ * @deprecated This will be removed in 2.0. Consider using the findUriFactory() method.
+ */
+ public static function findUrlFactory()
+ {
+ return static::findUriFactory();
+ }
+}
diff --git a/src/wp-includes/php-ai-client/third-party/Http/Discovery/Psr18Client.php b/src/wp-includes/php-ai-client/third-party/Http/Discovery/Psr18Client.php
new file mode 100644
index 0000000000000..83ed4ce970631
--- /dev/null
+++ b/src/wp-includes/php-ai-client/third-party/Http/Discovery/Psr18Client.php
@@ -0,0 +1,40 @@
+
+ */
+class Psr18Client extends Psr17Factory implements ClientInterface
+{
+ private $client;
+ public function __construct(?ClientInterface $client = null, ?RequestFactoryInterface $requestFactory = null, ?ResponseFactoryInterface $responseFactory = null, ?ServerRequestFactoryInterface $serverRequestFactory = null, ?StreamFactoryInterface $streamFactory = null, ?UploadedFileFactoryInterface $uploadedFileFactory = null, ?UriFactoryInterface $uriFactory = null)
+ {
+ $requestFactory ?? $requestFactory = $client instanceof RequestFactoryInterface ? $client : null;
+ $responseFactory ?? $responseFactory = $client instanceof ResponseFactoryInterface ? $client : null;
+ $serverRequestFactory ?? $serverRequestFactory = $client instanceof ServerRequestFactoryInterface ? $client : null;
+ $streamFactory ?? $streamFactory = $client instanceof StreamFactoryInterface ? $client : null;
+ $uploadedFileFactory ?? $uploadedFileFactory = $client instanceof UploadedFileFactoryInterface ? $client : null;
+ $uriFactory ?? $uriFactory = $client instanceof UriFactoryInterface ? $client : null;
+ parent::__construct($requestFactory, $responseFactory, $serverRequestFactory, $streamFactory, $uploadedFileFactory, $uriFactory);
+ $this->client = $client ?? Psr18ClientDiscovery::find();
+ }
+ public function sendRequest(RequestInterface $request): ResponseInterface
+ {
+ return $this->client->sendRequest($request);
+ }
+}
diff --git a/src/wp-includes/php-ai-client/third-party/Http/Discovery/Psr18ClientDiscovery.php b/src/wp-includes/php-ai-client/third-party/Http/Discovery/Psr18ClientDiscovery.php
new file mode 100644
index 0000000000000..9093e74df078b
--- /dev/null
+++ b/src/wp-includes/php-ai-client/third-party/Http/Discovery/Psr18ClientDiscovery.php
@@ -0,0 +1,31 @@
+
+ */
+final class Psr18ClientDiscovery extends ClassDiscovery
+{
+ /**
+ * Finds a PSR-18 HTTP Client.
+ *
+ * @return ClientInterface
+ *
+ * @throws RealNotFoundException
+ */
+ public static function find()
+ {
+ try {
+ $client = static::findOneByType(ClientInterface::class);
+ } catch (DiscoveryFailedException $e) {
+ throw new RealNotFoundException('No PSR-18 clients found. Make sure to install a package providing "psr/http-client-implementation". Example: "php-http/guzzle7-adapter".', 0, $e);
+ }
+ return static::instantiateClass($client);
+ }
+}
diff --git a/src/wp-includes/php-ai-client/third-party/Http/Discovery/Strategy/CommonClassesStrategy.php b/src/wp-includes/php-ai-client/third-party/Http/Discovery/Strategy/CommonClassesStrategy.php
new file mode 100644
index 0000000000000..02b3fdbf8a5b8
--- /dev/null
+++ b/src/wp-includes/php-ai-client/third-party/Http/Discovery/Strategy/CommonClassesStrategy.php
@@ -0,0 +1,116 @@
+
+ *
+ * Don't miss updating src/Composer/Plugin.php when adding a new supported class.
+ */
+final class CommonClassesStrategy implements DiscoveryStrategy
+{
+ /**
+ * @var array
+ */
+ private static $classes = [MessageFactory::class => [['class' => NyholmHttplugFactory::class, 'condition' => [NyholmHttplugFactory::class]], ['class' => GuzzleMessageFactory::class, 'condition' => [GuzzleRequest::class, GuzzleMessageFactory::class]], ['class' => DiactorosMessageFactory::class, 'condition' => [DiactorosRequest::class, DiactorosMessageFactory::class]], ['class' => SlimMessageFactory::class, 'condition' => [SlimRequest::class, SlimMessageFactory::class]]], StreamFactory::class => [['class' => NyholmHttplugFactory::class, 'condition' => [NyholmHttplugFactory::class]], ['class' => GuzzleStreamFactory::class, 'condition' => [GuzzleRequest::class, GuzzleStreamFactory::class]], ['class' => DiactorosStreamFactory::class, 'condition' => [DiactorosRequest::class, DiactorosStreamFactory::class]], ['class' => SlimStreamFactory::class, 'condition' => [SlimRequest::class, SlimStreamFactory::class]]], UriFactory::class => [['class' => NyholmHttplugFactory::class, 'condition' => [NyholmHttplugFactory::class]], ['class' => GuzzleUriFactory::class, 'condition' => [GuzzleRequest::class, GuzzleUriFactory::class]], ['class' => DiactorosUriFactory::class, 'condition' => [DiactorosRequest::class, DiactorosUriFactory::class]], ['class' => SlimUriFactory::class, 'condition' => [SlimRequest::class, SlimUriFactory::class]]], HttpAsyncClient::class => [['class' => SymfonyHttplug::class, 'condition' => [SymfonyHttplug::class, Promise::class, [self::class, 'isPsr17FactoryInstalled']]], ['class' => Guzzle7::class, 'condition' => Guzzle7::class], ['class' => Guzzle6::class, 'condition' => Guzzle6::class], ['class' => Curl::class, 'condition' => Curl::class], ['class' => React::class, 'condition' => React::class]], HttpClient::class => [['class' => SymfonyHttplug::class, 'condition' => [SymfonyHttplug::class, [self::class, 'isPsr17FactoryInstalled'], [self::class, 'isSymfonyImplementingHttpClient']]], ['class' => Guzzle7::class, 'condition' => Guzzle7::class], ['class' => Guzzle6::class, 'condition' => Guzzle6::class], ['class' => Guzzle5::class, 'condition' => Guzzle5::class], ['class' => Curl::class, 'condition' => Curl::class], ['class' => Socket::class, 'condition' => Socket::class], ['class' => Buzz::class, 'condition' => Buzz::class], ['class' => React::class, 'condition' => React::class], ['class' => Cake::class, 'condition' => Cake::class], ['class' => Artax::class, 'condition' => Artax::class], ['class' => [self::class, 'buzzInstantiate'], 'condition' => [\WordPress\AiClientDependencies\Buzz\Client\FileGetContents::class, \WordPress\AiClientDependencies\Buzz\Message\ResponseBuilder::class]]], Psr18Client::class => [['class' => [self::class, 'symfonyPsr18Instantiate'], 'condition' => [SymfonyPsr18::class, Psr17RequestFactory::class]], ['class' => GuzzleHttp::class, 'condition' => [self::class, 'isGuzzleImplementingPsr18']], ['class' => [self::class, 'buzzInstantiate'], 'condition' => [\WordPress\AiClientDependencies\Buzz\Client\FileGetContents::class, \WordPress\AiClientDependencies\Buzz\Message\ResponseBuilder::class]]]];
+ public static function getCandidates($type)
+ {
+ if (Psr18Client::class === $type) {
+ return self::getPsr18Candidates();
+ }
+ return self::$classes[$type] ?? [];
+ }
+ /**
+ * @return array The return value is always an array with zero or more elements. Each
+ * element is an array with two keys ['class' => string, 'condition' => mixed].
+ */
+ private static function getPsr18Candidates()
+ {
+ $candidates = self::$classes[Psr18Client::class];
+ // HTTPlug 2.0 clients implements PSR18Client too.
+ foreach (self::$classes[HttpClient::class] as $c) {
+ if (!is_string($c['class'])) {
+ continue;
+ }
+ try {
+ if (ClassDiscovery::safeClassExists($c['class']) && is_subclass_of($c['class'], Psr18Client::class)) {
+ $candidates[] = $c;
+ }
+ } catch (\Throwable $e) {
+ trigger_error(sprintf('Got exception "%s (%s)" while checking if a PSR-18 Client is available', get_class($e), $e->getMessage()), \E_USER_WARNING);
+ }
+ }
+ return $candidates;
+ }
+ public static function buzzInstantiate()
+ {
+ return new \WordPress\AiClientDependencies\Buzz\Client\FileGetContents(Psr17FactoryDiscovery::findResponseFactory());
+ }
+ public static function symfonyPsr18Instantiate()
+ {
+ return new SymfonyPsr18(null, Psr17FactoryDiscovery::findResponseFactory(), Psr17FactoryDiscovery::findStreamFactory());
+ }
+ public static function isGuzzleImplementingPsr18()
+ {
+ return defined('GuzzleHttp\ClientInterface::MAJOR_VERSION');
+ }
+ public static function isSymfonyImplementingHttpClient()
+ {
+ return is_subclass_of(SymfonyHttplug::class, HttpClient::class);
+ }
+ /**
+ * Can be used as a condition.
+ *
+ * @return bool
+ */
+ public static function isPsr17FactoryInstalled()
+ {
+ try {
+ Psr17FactoryDiscovery::findResponseFactory();
+ } catch (NotFoundException $e) {
+ return \false;
+ } catch (\Throwable $e) {
+ trigger_error(sprintf('Got exception "%s (%s)" while checking if a PSR-17 ResponseFactory is available', get_class($e), $e->getMessage()), \E_USER_WARNING);
+ return \false;
+ }
+ return \true;
+ }
+}
diff --git a/src/wp-includes/php-ai-client/third-party/Http/Discovery/Strategy/CommonPsr17ClassesStrategy.php b/src/wp-includes/php-ai-client/third-party/Http/Discovery/Strategy/CommonPsr17ClassesStrategy.php
new file mode 100644
index 0000000000000..3e5227f6d56ce
--- /dev/null
+++ b/src/wp-includes/php-ai-client/third-party/Http/Discovery/Strategy/CommonPsr17ClassesStrategy.php
@@ -0,0 +1,34 @@
+
+ *
+ * Don't miss updating src/Composer/Plugin.php when adding a new supported class.
+ */
+final class CommonPsr17ClassesStrategy implements DiscoveryStrategy
+{
+ /**
+ * @var array
+ */
+ private static $classes = [RequestFactoryInterface::class => ['Phalcon\Http\Message\RequestFactory', 'Nyholm\Psr7\Factory\Psr17Factory', 'GuzzleHttp\Psr7\HttpFactory', 'WordPress\AiClientDependencies\Http\Factory\Diactoros\RequestFactory', 'WordPress\AiClientDependencies\Http\Factory\Guzzle\RequestFactory', 'WordPress\AiClientDependencies\Http\Factory\Slim\RequestFactory', 'Laminas\Diactoros\RequestFactory', 'Slim\Psr7\Factory\RequestFactory', 'WordPress\AiClientDependencies\HttpSoft\Message\RequestFactory'], ResponseFactoryInterface::class => ['Phalcon\Http\Message\ResponseFactory', 'Nyholm\Psr7\Factory\Psr17Factory', 'GuzzleHttp\Psr7\HttpFactory', 'WordPress\AiClientDependencies\Http\Factory\Diactoros\ResponseFactory', 'WordPress\AiClientDependencies\Http\Factory\Guzzle\ResponseFactory', 'WordPress\AiClientDependencies\Http\Factory\Slim\ResponseFactory', 'Laminas\Diactoros\ResponseFactory', 'Slim\Psr7\Factory\ResponseFactory', 'WordPress\AiClientDependencies\HttpSoft\Message\ResponseFactory'], ServerRequestFactoryInterface::class => ['Phalcon\Http\Message\ServerRequestFactory', 'Nyholm\Psr7\Factory\Psr17Factory', 'GuzzleHttp\Psr7\HttpFactory', 'WordPress\AiClientDependencies\Http\Factory\Diactoros\ServerRequestFactory', 'WordPress\AiClientDependencies\Http\Factory\Guzzle\ServerRequestFactory', 'WordPress\AiClientDependencies\Http\Factory\Slim\ServerRequestFactory', 'Laminas\Diactoros\ServerRequestFactory', 'Slim\Psr7\Factory\ServerRequestFactory', 'WordPress\AiClientDependencies\HttpSoft\Message\ServerRequestFactory'], StreamFactoryInterface::class => ['Phalcon\Http\Message\StreamFactory', 'Nyholm\Psr7\Factory\Psr17Factory', 'GuzzleHttp\Psr7\HttpFactory', 'WordPress\AiClientDependencies\Http\Factory\Diactoros\StreamFactory', 'WordPress\AiClientDependencies\Http\Factory\Guzzle\StreamFactory', 'WordPress\AiClientDependencies\Http\Factory\Slim\StreamFactory', 'Laminas\Diactoros\StreamFactory', 'Slim\Psr7\Factory\StreamFactory', 'WordPress\AiClientDependencies\HttpSoft\Message\StreamFactory'], UploadedFileFactoryInterface::class => ['Phalcon\Http\Message\UploadedFileFactory', 'Nyholm\Psr7\Factory\Psr17Factory', 'GuzzleHttp\Psr7\HttpFactory', 'WordPress\AiClientDependencies\Http\Factory\Diactoros\UploadedFileFactory', 'WordPress\AiClientDependencies\Http\Factory\Guzzle\UploadedFileFactory', 'WordPress\AiClientDependencies\Http\Factory\Slim\UploadedFileFactory', 'Laminas\Diactoros\UploadedFileFactory', 'Slim\Psr7\Factory\UploadedFileFactory', 'WordPress\AiClientDependencies\HttpSoft\Message\UploadedFileFactory'], UriFactoryInterface::class => ['Phalcon\Http\Message\UriFactory', 'Nyholm\Psr7\Factory\Psr17Factory', 'GuzzleHttp\Psr7\HttpFactory', 'WordPress\AiClientDependencies\Http\Factory\Diactoros\UriFactory', 'WordPress\AiClientDependencies\Http\Factory\Guzzle\UriFactory', 'WordPress\AiClientDependencies\Http\Factory\Slim\UriFactory', 'Laminas\Diactoros\UriFactory', 'Slim\Psr7\Factory\UriFactory', 'WordPress\AiClientDependencies\HttpSoft\Message\UriFactory']];
+ public static function getCandidates($type)
+ {
+ $candidates = [];
+ if (isset(self::$classes[$type])) {
+ foreach (self::$classes[$type] as $class) {
+ $candidates[] = ['class' => $class, 'condition' => [$class]];
+ }
+ }
+ return $candidates;
+ }
+}
diff --git a/src/wp-includes/php-ai-client/third-party/Http/Discovery/Strategy/DiscoveryStrategy.php b/src/wp-includes/php-ai-client/third-party/Http/Discovery/Strategy/DiscoveryStrategy.php
new file mode 100644
index 0000000000000..d7f782db42df7
--- /dev/null
+++ b/src/wp-includes/php-ai-client/third-party/Http/Discovery/Strategy/DiscoveryStrategy.php
@@ -0,0 +1,22 @@
+
+ */
+interface DiscoveryStrategy
+{
+ /**
+ * Find a resource of a specific type.
+ *
+ * @param string $type
+ *
+ * @return array The return value is always an array with zero or more elements. Each
+ * element is an array with two keys ['class' => string, 'condition' => mixed].
+ *
+ * @throws StrategyUnavailableException if we cannot use this strategy
+ */
+ public static function getCandidates($type);
+}
diff --git a/src/wp-includes/php-ai-client/third-party/Http/Discovery/Strategy/MockClientStrategy.php b/src/wp-includes/php-ai-client/third-party/Http/Discovery/Strategy/MockClientStrategy.php
new file mode 100644
index 0000000000000..3c05c3dce8db2
--- /dev/null
+++ b/src/wp-includes/php-ai-client/third-party/Http/Discovery/Strategy/MockClientStrategy.php
@@ -0,0 +1,22 @@
+
+ */
+final class MockClientStrategy implements DiscoveryStrategy
+{
+ public static function getCandidates($type)
+ {
+ if (is_a(HttpClient::class, $type, \true) || is_a(HttpAsyncClient::class, $type, \true)) {
+ return [['class' => Mock::class, 'condition' => Mock::class]];
+ }
+ return [];
+ }
+}
diff --git a/src/wp-includes/php-ai-client/third-party/Http/Discovery/Strategy/PuliBetaStrategy.php b/src/wp-includes/php-ai-client/third-party/Http/Discovery/Strategy/PuliBetaStrategy.php
new file mode 100644
index 0000000000000..bdcfc82344514
--- /dev/null
+++ b/src/wp-includes/php-ai-client/third-party/Http/Discovery/Strategy/PuliBetaStrategy.php
@@ -0,0 +1,77 @@
+
+ * @author Márk Sági-Kazár
+ */
+class PuliBetaStrategy implements DiscoveryStrategy
+{
+ /**
+ * @var GeneratedPuliFactory
+ */
+ protected static $puliFactory;
+ /**
+ * @var Discovery
+ */
+ protected static $puliDiscovery;
+ /**
+ * @return GeneratedPuliFactory
+ *
+ * @throws PuliUnavailableException
+ */
+ private static function getPuliFactory()
+ {
+ if (null === self::$puliFactory) {
+ if (!defined('PULI_FACTORY_CLASS')) {
+ throw new PuliUnavailableException('Puli Factory is not available');
+ }
+ $puliFactoryClass = PULI_FACTORY_CLASS;
+ if (!ClassDiscovery::safeClassExists($puliFactoryClass)) {
+ throw new PuliUnavailableException('Puli Factory class does not exist');
+ }
+ self::$puliFactory = new $puliFactoryClass();
+ }
+ return self::$puliFactory;
+ }
+ /**
+ * Returns the Puli discovery layer.
+ *
+ * @return Discovery
+ *
+ * @throws PuliUnavailableException
+ */
+ private static function getPuliDiscovery()
+ {
+ if (!isset(self::$puliDiscovery)) {
+ $factory = self::getPuliFactory();
+ $repository = $factory->createRepository();
+ self::$puliDiscovery = $factory->createDiscovery($repository);
+ }
+ return self::$puliDiscovery;
+ }
+ public static function getCandidates($type)
+ {
+ $returnData = [];
+ $bindings = self::getPuliDiscovery()->findBindings($type);
+ foreach ($bindings as $binding) {
+ $condition = \true;
+ if ($binding->hasParameterValue('depends')) {
+ $condition = $binding->getParameterValue('depends');
+ }
+ $returnData[] = ['class' => $binding->getClassName(), 'condition' => $condition];
+ }
+ return $returnData;
+ }
+}
diff --git a/src/wp-includes/php-ai-client/third-party/Http/Discovery/StreamFactoryDiscovery.php b/src/wp-includes/php-ai-client/third-party/Http/Discovery/StreamFactoryDiscovery.php
new file mode 100644
index 0000000000000..770dd80b4ae80
--- /dev/null
+++ b/src/wp-includes/php-ai-client/third-party/Http/Discovery/StreamFactoryDiscovery.php
@@ -0,0 +1,32 @@
+
+ *
+ * @deprecated This will be removed in 2.0. Consider using Psr17FactoryDiscovery.
+ */
+final class StreamFactoryDiscovery extends ClassDiscovery
+{
+ /**
+ * Finds a Stream Factory.
+ *
+ * @return StreamFactory
+ *
+ * @throws Exception\NotFoundException
+ */
+ public static function find()
+ {
+ try {
+ $streamFactory = static::findOneByType(StreamFactory::class);
+ } catch (DiscoveryFailedException $e) {
+ throw new NotFoundException('No stream factories found. To use Guzzle, Diactoros or Slim Framework factories install php-http/message and the chosen message implementation.', 0, $e);
+ }
+ return static::instantiateClass($streamFactory);
+ }
+}
diff --git a/src/wp-includes/php-ai-client/third-party/Http/Discovery/UriFactoryDiscovery.php b/src/wp-includes/php-ai-client/third-party/Http/Discovery/UriFactoryDiscovery.php
new file mode 100644
index 0000000000000..8847fa4942c4d
--- /dev/null
+++ b/src/wp-includes/php-ai-client/third-party/Http/Discovery/UriFactoryDiscovery.php
@@ -0,0 +1,32 @@
+
+ *
+ * @deprecated This will be removed in 2.0. Consider using Psr17FactoryDiscovery.
+ */
+final class UriFactoryDiscovery extends ClassDiscovery
+{
+ /**
+ * Finds a URI Factory.
+ *
+ * @return UriFactory
+ *
+ * @throws Exception\NotFoundException
+ */
+ public static function find()
+ {
+ try {
+ $uriFactory = static::findOneByType(UriFactory::class);
+ } catch (DiscoveryFailedException $e) {
+ throw new NotFoundException('No uri factories found. To use Guzzle, Diactoros or Slim Framework factories install php-http/message and the chosen message implementation.', 0, $e);
+ }
+ return static::instantiateClass($uriFactory);
+ }
+}
diff --git a/src/wp-includes/php-ai-client/third-party/Http/Promise/FulfilledPromise.php b/src/wp-includes/php-ai-client/third-party/Http/Promise/FulfilledPromise.php
new file mode 100644
index 0000000000000..663b091a4e57a
--- /dev/null
+++ b/src/wp-includes/php-ai-client/third-party/Http/Promise/FulfilledPromise.php
@@ -0,0 +1,45 @@
+
+ */
+final class FulfilledPromise implements Promise
+{
+ /**
+ * @var mixed
+ */
+ private $result;
+ /**
+ * @param mixed $result
+ */
+ public function __construct($result)
+ {
+ $this->result = $result;
+ }
+ public function then(?callable $onFulfilled = null, ?callable $onRejected = null)
+ {
+ if (null === $onFulfilled) {
+ return $this;
+ }
+ try {
+ return new self($onFulfilled($this->result));
+ } catch (\Exception $e) {
+ return new RejectedPromise($e);
+ }
+ }
+ public function getState()
+ {
+ return Promise::FULFILLED;
+ }
+ public function wait($unwrap = \true)
+ {
+ if ($unwrap) {
+ return $this->result;
+ }
+ return null;
+ }
+}
diff --git a/src/wp-includes/php-ai-client/third-party/Http/Promise/Promise.php b/src/wp-includes/php-ai-client/third-party/Http/Promise/Promise.php
new file mode 100644
index 0000000000000..8c3dcb452300a
--- /dev/null
+++ b/src/wp-includes/php-ai-client/third-party/Http/Promise/Promise.php
@@ -0,0 +1,64 @@
+
+ * @author Márk Sági-Kazár
+ */
+interface Promise
+{
+ /**
+ * Promise has not been fulfilled or rejected.
+ */
+ const PENDING = 'pending';
+ /**
+ * Promise has been fulfilled.
+ */
+ const FULFILLED = 'fulfilled';
+ /**
+ * Promise has been rejected.
+ */
+ const REJECTED = 'rejected';
+ /**
+ * Adds behavior for when the promise is resolved or rejected (response will be available, or error happens).
+ *
+ * If you do not care about one of the cases, you can set the corresponding callable to null
+ * The callback will be called when the value arrived and never more than once.
+ *
+ * @param callable|null $onFulfilled called when a response will be available
+ * @param callable|null $onRejected called when an exception occurs
+ *
+ * @return Promise a new resolved promise with value of the executed callback (onFulfilled / onRejected)
+ */
+ public function then(?callable $onFulfilled = null, ?callable $onRejected = null);
+ /**
+ * Returns the state of the promise, one of PENDING, FULFILLED or REJECTED.
+ *
+ * @return string
+ */
+ public function getState();
+ /**
+ * Wait for the promise to be fulfilled or rejected.
+ *
+ * When this method returns, the request has been resolved and if callables have been
+ * specified, the appropriate one has terminated.
+ *
+ * When $unwrap is true (the default), the response is returned, or the exception thrown
+ * on failure. Otherwise, nothing is returned or thrown.
+ *
+ * @param bool $unwrap Whether to return resolved value / throw reason or not
+ *
+ * @return ($unwrap is true ? mixed : null) Resolved value, null if $unwrap is set to false
+ *
+ * @throws \Throwable the rejection reason if $unwrap is set to true and the request failed
+ */
+ public function wait($unwrap = \true);
+}
diff --git a/src/wp-includes/php-ai-client/third-party/Http/Promise/RejectedPromise.php b/src/wp-includes/php-ai-client/third-party/Http/Promise/RejectedPromise.php
new file mode 100644
index 0000000000000..f1d8e2f9a173c
--- /dev/null
+++ b/src/wp-includes/php-ai-client/third-party/Http/Promise/RejectedPromise.php
@@ -0,0 +1,42 @@
+
+ */
+final class RejectedPromise implements Promise
+{
+ /**
+ * @var \Throwable
+ */
+ private $exception;
+ public function __construct(\Throwable $exception)
+ {
+ $this->exception = $exception;
+ }
+ public function then(?callable $onFulfilled = null, ?callable $onRejected = null)
+ {
+ if (null === $onRejected) {
+ return $this;
+ }
+ try {
+ return new FulfilledPromise($onRejected($this->exception));
+ } catch (\Exception $e) {
+ return new self($e);
+ }
+ }
+ public function getState()
+ {
+ return Promise::REJECTED;
+ }
+ public function wait($unwrap = \true)
+ {
+ if ($unwrap) {
+ throw $this->exception;
+ }
+ return null;
+ }
+}
diff --git a/src/wp-includes/php-ai-client/third-party/Psr/EventDispatcher/EventDispatcherInterface.php b/src/wp-includes/php-ai-client/third-party/Psr/EventDispatcher/EventDispatcherInterface.php
new file mode 100644
index 0000000000000..d522445fce250
--- /dev/null
+++ b/src/wp-includes/php-ai-client/third-party/Psr/EventDispatcher/EventDispatcherInterface.php
@@ -0,0 +1,21 @@
+getHeaders() as $name => $values) {
+ * echo $name . ": " . implode(", ", $values);
+ * }
+ *
+ * // Emit headers iteratively:
+ * foreach ($message->getHeaders() as $name => $values) {
+ * foreach ($values as $value) {
+ * header(sprintf('%s: %s', $name, $value), false);
+ * }
+ * }
+ *
+ * While header names are not case-sensitive, getHeaders() will preserve the
+ * exact case in which headers were originally specified.
+ *
+ * @return string[][] Returns an associative array of the message's headers. Each
+ * key MUST be a header name, and each value MUST be an array of strings
+ * for that header.
+ */
+ public function getHeaders(): array;
+ /**
+ * Checks if a header exists by the given case-insensitive name.
+ *
+ * @param string $name Case-insensitive header field name.
+ * @return bool Returns true if any header names match the given header
+ * name using a case-insensitive string comparison. Returns false if
+ * no matching header name is found in the message.
+ */
+ public function hasHeader(string $name): bool;
+ /**
+ * Retrieves a message header value by the given case-insensitive name.
+ *
+ * This method returns an array of all the header values of the given
+ * case-insensitive header name.
+ *
+ * If the header does not appear in the message, this method MUST return an
+ * empty array.
+ *
+ * @param string $name Case-insensitive header field name.
+ * @return string[] An array of string values as provided for the given
+ * header. If the header does not appear in the message, this method MUST
+ * return an empty array.
+ */
+ public function getHeader(string $name): array;
+ /**
+ * Retrieves a comma-separated string of the values for a single header.
+ *
+ * This method returns all of the header values of the given
+ * case-insensitive header name as a string concatenated together using
+ * a comma.
+ *
+ * NOTE: Not all header values may be appropriately represented using
+ * comma concatenation. For such headers, use getHeader() instead
+ * and supply your own delimiter when concatenating.
+ *
+ * If the header does not appear in the message, this method MUST return
+ * an empty string.
+ *
+ * @param string $name Case-insensitive header field name.
+ * @return string A string of values as provided for the given header
+ * concatenated together using a comma. If the header does not appear in
+ * the message, this method MUST return an empty string.
+ */
+ public function getHeaderLine(string $name): string;
+ /**
+ * Return an instance with the provided value replacing the specified header.
+ *
+ * While header names are case-insensitive, the casing of the header will
+ * be preserved by this function, and returned from getHeaders().
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * new and/or updated header and value.
+ *
+ * @param string $name Case-insensitive header field name.
+ * @param string|string[] $value Header value(s).
+ * @return static
+ * @throws \InvalidArgumentException for invalid header names or values.
+ */
+ public function withHeader(string $name, $value): \Psr\Http\Message\MessageInterface;
+ /**
+ * Return an instance with the specified header appended with the given value.
+ *
+ * Existing values for the specified header will be maintained. The new
+ * value(s) will be appended to the existing list. If the header did not
+ * exist previously, it will be added.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * new header and/or value.
+ *
+ * @param string $name Case-insensitive header field name to add.
+ * @param string|string[] $value Header value(s).
+ * @return static
+ * @throws \InvalidArgumentException for invalid header names or values.
+ */
+ public function withAddedHeader(string $name, $value): \Psr\Http\Message\MessageInterface;
+ /**
+ * Return an instance without the specified header.
+ *
+ * Header resolution MUST be done without case-sensitivity.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that removes
+ * the named header.
+ *
+ * @param string $name Case-insensitive header field name to remove.
+ * @return static
+ */
+ public function withoutHeader(string $name): \Psr\Http\Message\MessageInterface;
+ /**
+ * Gets the body of the message.
+ *
+ * @return StreamInterface Returns the body as a stream.
+ */
+ public function getBody(): \Psr\Http\Message\StreamInterface;
+ /**
+ * Return an instance with the specified message body.
+ *
+ * The body MUST be a StreamInterface object.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return a new instance that has the
+ * new body stream.
+ *
+ * @param StreamInterface $body Body.
+ * @return static
+ * @throws \InvalidArgumentException When the body is not valid.
+ */
+ public function withBody(\Psr\Http\Message\StreamInterface $body): \Psr\Http\Message\MessageInterface;
+}
diff --git a/src/wp-includes/php-ai-client/third-party/Psr/Http/Message/RequestFactoryInterface.php b/src/wp-includes/php-ai-client/third-party/Psr/Http/Message/RequestFactoryInterface.php
new file mode 100644
index 0000000000000..b06c80eb405b0
--- /dev/null
+++ b/src/wp-includes/php-ai-client/third-party/Psr/Http/Message/RequestFactoryInterface.php
@@ -0,0 +1,18 @@
+getQuery()`
+ * or from the `QUERY_STRING` server param.
+ *
+ * @return array
+ */
+ public function getQueryParams(): array;
+ /**
+ * Return an instance with the specified query string arguments.
+ *
+ * These values SHOULD remain immutable over the course of the incoming
+ * request. They MAY be injected during instantiation, such as from PHP's
+ * $_GET superglobal, or MAY be derived from some other value such as the
+ * URI. In cases where the arguments are parsed from the URI, the data
+ * MUST be compatible with what PHP's parse_str() would return for
+ * purposes of how duplicate query parameters are handled, and how nested
+ * sets are handled.
+ *
+ * Setting query string arguments MUST NOT change the URI stored by the
+ * request, nor the values in the server params.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated query string arguments.
+ *
+ * @param array $query Array of query string arguments, typically from
+ * $_GET.
+ * @return static
+ */
+ public function withQueryParams(array $query): \Psr\Http\Message\ServerRequestInterface;
+ /**
+ * Retrieve normalized file upload data.
+ *
+ * This method returns upload metadata in a normalized tree, with each leaf
+ * an instance of Psr\Http\Message\UploadedFileInterface.
+ *
+ * These values MAY be prepared from $_FILES or the message body during
+ * instantiation, or MAY be injected via withUploadedFiles().
+ *
+ * @return array An array tree of UploadedFileInterface instances; an empty
+ * array MUST be returned if no data is present.
+ */
+ public function getUploadedFiles(): array;
+ /**
+ * Create a new instance with the specified uploaded files.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated body parameters.
+ *
+ * @param array $uploadedFiles An array tree of UploadedFileInterface instances.
+ * @return static
+ * @throws \InvalidArgumentException if an invalid structure is provided.
+ */
+ public function withUploadedFiles(array $uploadedFiles): \Psr\Http\Message\ServerRequestInterface;
+ /**
+ * Retrieve any parameters provided in the request body.
+ *
+ * If the request Content-Type is either application/x-www-form-urlencoded
+ * or multipart/form-data, and the request method is POST, this method MUST
+ * return the contents of $_POST.
+ *
+ * Otherwise, this method may return any results of deserializing
+ * the request body content; as parsing returns structured content, the
+ * potential types MUST be arrays or objects only. A null value indicates
+ * the absence of body content.
+ *
+ * @return null|array|object The deserialized body parameters, if any.
+ * These will typically be an array or object.
+ */
+ public function getParsedBody();
+ /**
+ * Return an instance with the specified body parameters.
+ *
+ * These MAY be injected during instantiation.
+ *
+ * If the request Content-Type is either application/x-www-form-urlencoded
+ * or multipart/form-data, and the request method is POST, use this method
+ * ONLY to inject the contents of $_POST.
+ *
+ * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of
+ * deserializing the request body content. Deserialization/parsing returns
+ * structured data, and, as such, this method ONLY accepts arrays or objects,
+ * or a null value if nothing was available to parse.
+ *
+ * As an example, if content negotiation determines that the request data
+ * is a JSON payload, this method could be used to create a request
+ * instance with the deserialized parameters.
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated body parameters.
+ *
+ * @param null|array|object $data The deserialized body data. This will
+ * typically be in an array or object.
+ * @return static
+ * @throws \InvalidArgumentException if an unsupported argument type is
+ * provided.
+ */
+ public function withParsedBody($data): \Psr\Http\Message\ServerRequestInterface;
+ /**
+ * Retrieve attributes derived from the request.
+ *
+ * The request "attributes" may be used to allow injection of any
+ * parameters derived from the request: e.g., the results of path
+ * match operations; the results of decrypting cookies; the results of
+ * deserializing non-form-encoded message bodies; etc. Attributes
+ * will be application and request specific, and CAN be mutable.
+ *
+ * @return array Attributes derived from the request.
+ */
+ public function getAttributes(): array;
+ /**
+ * Retrieve a single derived request attribute.
+ *
+ * Retrieves a single derived request attribute as described in
+ * getAttributes(). If the attribute has not been previously set, returns
+ * the default value as provided.
+ *
+ * This method obviates the need for a hasAttribute() method, as it allows
+ * specifying a default value to return if the attribute is not found.
+ *
+ * @see getAttributes()
+ * @param string $name The attribute name.
+ * @param mixed $default Default value to return if the attribute does not exist.
+ * @return mixed
+ */
+ public function getAttribute(string $name, $default = null);
+ /**
+ * Return an instance with the specified derived request attribute.
+ *
+ * This method allows setting a single derived request attribute as
+ * described in getAttributes().
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that has the
+ * updated attribute.
+ *
+ * @see getAttributes()
+ * @param string $name The attribute name.
+ * @param mixed $value The value of the attribute.
+ * @return static
+ */
+ public function withAttribute(string $name, $value): \Psr\Http\Message\ServerRequestInterface;
+ /**
+ * Return an instance that removes the specified derived request attribute.
+ *
+ * This method allows removing a single derived request attribute as
+ * described in getAttributes().
+ *
+ * This method MUST be implemented in such a way as to retain the
+ * immutability of the message, and MUST return an instance that removes
+ * the attribute.
+ *
+ * @see getAttributes()
+ * @param string $name The attribute name.
+ * @return static
+ */
+ public function withoutAttribute(string $name): \Psr\Http\Message\ServerRequestInterface;
+}
diff --git a/src/wp-includes/php-ai-client/third-party/Psr/Http/Message/StreamFactoryInterface.php b/src/wp-includes/php-ai-client/third-party/Psr/Http/Message/StreamFactoryInterface.php
new file mode 100644
index 0000000000000..42d3fb70710a9
--- /dev/null
+++ b/src/wp-includes/php-ai-client/third-party/Psr/Http/Message/StreamFactoryInterface.php
@@ -0,0 +1,43 @@
+
+ * [user-info@]host[:port]
+ *
+ *
+ * If the port component is not set or is the standard port for the current
+ * scheme, it SHOULD NOT be included.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-3.2
+ * @return string The URI authority, in "[user-info@]host[:port]" format.
+ */
+ public function getAuthority(): string;
+ /**
+ * Retrieve the user information component of the URI.
+ *
+ * If no user information is present, this method MUST return an empty
+ * string.
+ *
+ * If a user is present in the URI, this will return that value;
+ * additionally, if the password is also present, it will be appended to the
+ * user value, with a colon (":") separating the values.
+ *
+ * The trailing "@" character is not part of the user information and MUST
+ * NOT be added.
+ *
+ * @return string The URI user information, in "username[:password]" format.
+ */
+ public function getUserInfo(): string;
+ /**
+ * Retrieve the host component of the URI.
+ *
+ * If no host is present, this method MUST return an empty string.
+ *
+ * The value returned MUST be normalized to lowercase, per RFC 3986
+ * Section 3.2.2.
+ *
+ * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
+ * @return string The URI host.
+ */
+ public function getHost(): string;
+ /**
+ * Retrieve the port component of the URI.
+ *
+ * If a port is present, and it is non-standard for the current scheme,
+ * this method MUST return it as an integer. If the port is the standard port
+ * used with the current scheme, this method SHOULD return null.
+ *
+ * If no port is present, and no scheme is present, this method MUST return
+ * a null value.
+ *
+ * If no port is present, but a scheme is present, this method MAY return
+ * the standard port for that scheme, but SHOULD return null.
+ *
+ * @return null|int The URI port.
+ */
+ public function getPort(): ?int;
+ /**
+ * Retrieve the path component of the URI.
+ *
+ * The path can either be empty or absolute (starting with a slash) or
+ * rootless (not starting with a slash). Implementations MUST support all
+ * three syntaxes.
+ *
+ * Normally, the empty path "" and absolute path "/" are considered equal as
+ * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
+ * do this normalization because in contexts with a trimmed base path, e.g.
+ * the front controller, this difference becomes significant. It's the task
+ * of the user to handle both "" and "/".
+ *
+ * The value returned MUST be percent-encoded, but MUST NOT double-encode
+ * any characters. To determine what characters to encode, please refer to
+ * RFC 3986, Sections 2 and 3.3.
+ *
+ * As an example, if the value should include a slash ("/") not intended as
+ * delimiter between path segments, that value MUST be passed in encoded
+ * form (e.g., "%2F") to the instance.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-2
+ * @see https://tools.ietf.org/html/rfc3986#section-3.3
+ * @return string The URI path.
+ */
+ public function getPath(): string;
+ /**
+ * Retrieve the query string of the URI.
+ *
+ * If no query string is present, this method MUST return an empty string.
+ *
+ * The leading "?" character is not part of the query and MUST NOT be
+ * added.
+ *
+ * The value returned MUST be percent-encoded, but MUST NOT double-encode
+ * any characters. To determine what characters to encode, please refer to
+ * RFC 3986, Sections 2 and 3.4.
+ *
+ * As an example, if a value in a key/value pair of the query string should
+ * include an ampersand ("&") not intended as a delimiter between values,
+ * that value MUST be passed in encoded form (e.g., "%26") to the instance.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-2
+ * @see https://tools.ietf.org/html/rfc3986#section-3.4
+ * @return string The URI query string.
+ */
+ public function getQuery(): string;
+ /**
+ * Retrieve the fragment component of the URI.
+ *
+ * If no fragment is present, this method MUST return an empty string.
+ *
+ * The leading "#" character is not part of the fragment and MUST NOT be
+ * added.
+ *
+ * The value returned MUST be percent-encoded, but MUST NOT double-encode
+ * any characters. To determine what characters to encode, please refer to
+ * RFC 3986, Sections 2 and 3.5.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-2
+ * @see https://tools.ietf.org/html/rfc3986#section-3.5
+ * @return string The URI fragment.
+ */
+ public function getFragment(): string;
+ /**
+ * Return an instance with the specified scheme.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified scheme.
+ *
+ * Implementations MUST support the schemes "http" and "https" case
+ * insensitively, and MAY accommodate other schemes if required.
+ *
+ * An empty scheme is equivalent to removing the scheme.
+ *
+ * @param string $scheme The scheme to use with the new instance.
+ * @return static A new instance with the specified scheme.
+ * @throws \InvalidArgumentException for invalid or unsupported schemes.
+ */
+ public function withScheme(string $scheme): \Psr\Http\Message\UriInterface;
+ /**
+ * Return an instance with the specified user information.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified user information.
+ *
+ * Password is optional, but the user information MUST include the
+ * user; an empty string for the user is equivalent to removing user
+ * information.
+ *
+ * @param string $user The user name to use for authority.
+ * @param null|string $password The password associated with $user.
+ * @return static A new instance with the specified user information.
+ */
+ public function withUserInfo(string $user, ?string $password = null): \Psr\Http\Message\UriInterface;
+ /**
+ * Return an instance with the specified host.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified host.
+ *
+ * An empty host value is equivalent to removing the host.
+ *
+ * @param string $host The hostname to use with the new instance.
+ * @return static A new instance with the specified host.
+ * @throws \InvalidArgumentException for invalid hostnames.
+ */
+ public function withHost(string $host): \Psr\Http\Message\UriInterface;
+ /**
+ * Return an instance with the specified port.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified port.
+ *
+ * Implementations MUST raise an exception for ports outside the
+ * established TCP and UDP port ranges.
+ *
+ * A null value provided for the port is equivalent to removing the port
+ * information.
+ *
+ * @param null|int $port The port to use with the new instance; a null value
+ * removes the port information.
+ * @return static A new instance with the specified port.
+ * @throws \InvalidArgumentException for invalid ports.
+ */
+ public function withPort(?int $port): \Psr\Http\Message\UriInterface;
+ /**
+ * Return an instance with the specified path.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified path.
+ *
+ * The path can either be empty or absolute (starting with a slash) or
+ * rootless (not starting with a slash). Implementations MUST support all
+ * three syntaxes.
+ *
+ * If the path is intended to be domain-relative rather than path relative then
+ * it must begin with a slash ("/"). Paths not starting with a slash ("/")
+ * are assumed to be relative to some base path known to the application or
+ * consumer.
+ *
+ * Users can provide both encoded and decoded path characters.
+ * Implementations ensure the correct encoding as outlined in getPath().
+ *
+ * @param string $path The path to use with the new instance.
+ * @return static A new instance with the specified path.
+ * @throws \InvalidArgumentException for invalid paths.
+ */
+ public function withPath(string $path): \Psr\Http\Message\UriInterface;
+ /**
+ * Return an instance with the specified query string.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified query string.
+ *
+ * Users can provide both encoded and decoded query characters.
+ * Implementations ensure the correct encoding as outlined in getQuery().
+ *
+ * An empty query string value is equivalent to removing the query string.
+ *
+ * @param string $query The query string to use with the new instance.
+ * @return static A new instance with the specified query string.
+ * @throws \InvalidArgumentException for invalid query strings.
+ */
+ public function withQuery(string $query): \Psr\Http\Message\UriInterface;
+ /**
+ * Return an instance with the specified URI fragment.
+ *
+ * This method MUST retain the state of the current instance, and return
+ * an instance that contains the specified URI fragment.
+ *
+ * Users can provide both encoded and decoded fragment characters.
+ * Implementations ensure the correct encoding as outlined in getFragment().
+ *
+ * An empty fragment value is equivalent to removing the fragment.
+ *
+ * @param string $fragment The fragment to use with the new instance.
+ * @return static A new instance with the specified fragment.
+ */
+ public function withFragment(string $fragment): \Psr\Http\Message\UriInterface;
+ /**
+ * Return the string representation as a URI reference.
+ *
+ * Depending on which components of the URI are present, the resulting
+ * string is either a full URI or relative reference according to RFC 3986,
+ * Section 4.1. The method concatenates the various components of the URI,
+ * using the appropriate delimiters:
+ *
+ * - If a scheme is present, it MUST be suffixed by ":".
+ * - If an authority is present, it MUST be prefixed by "//".
+ * - The path can be concatenated without delimiters. But there are two
+ * cases where the path has to be adjusted to make the URI reference
+ * valid as PHP does not allow to throw an exception in __toString():
+ * - If the path is rootless and an authority is present, the path MUST
+ * be prefixed by "/".
+ * - If the path is starting with more than one "/" and no authority is
+ * present, the starting slashes MUST be reduced to one.
+ * - If a query is present, it MUST be prefixed by "?".
+ * - If a fragment is present, it MUST be prefixed by "#".
+ *
+ * @see http://tools.ietf.org/html/rfc3986#section-4.1
+ * @return string
+ */
+ public function __toString(): string;
+}
diff --git a/src/wp-includes/php-ai-client/third-party/Psr/SimpleCache/CacheException.php b/src/wp-includes/php-ai-client/third-party/Psr/SimpleCache/CacheException.php
new file mode 100644
index 0000000000000..eba53815c0c98
--- /dev/null
+++ b/src/wp-includes/php-ai-client/third-party/Psr/SimpleCache/CacheException.php
@@ -0,0 +1,10 @@
+ value pairs. Cache keys that do not exist or are stale will have $default as value.
+ *
+ * @throws \Psr\SimpleCache\InvalidArgumentException
+ * MUST be thrown if $keys is neither an array nor a Traversable,
+ * or if any of the $keys are not a legal value.
+ */
+ public function getMultiple($keys, $default = null);
+ /**
+ * Persists a set of key => value pairs in the cache, with an optional TTL.
+ *
+ * @param iterable $values A list of key => value pairs for a multiple-set operation.
+ * @param null|int|\DateInterval $ttl Optional. The TTL value of this item. If no value is sent and
+ * the driver supports TTL then the library may set a default value
+ * for it or let the driver take care of that.
+ *
+ * @return bool True on success and false on failure.
+ *
+ * @throws \Psr\SimpleCache\InvalidArgumentException
+ * MUST be thrown if $values is neither an array nor a Traversable,
+ * or if any of the $values are not a legal value.
+ */
+ public function setMultiple($values, $ttl = null);
+ /**
+ * Deletes multiple cache items in a single operation.
+ *
+ * @param iterable $keys A list of string-based keys to be deleted.
+ *
+ * @return bool True if the items were successfully removed. False if there was an error.
+ *
+ * @throws \Psr\SimpleCache\InvalidArgumentException
+ * MUST be thrown if $keys is neither an array nor a Traversable,
+ * or if any of the $keys are not a legal value.
+ */
+ public function deleteMultiple($keys);
+ /**
+ * Determines whether an item is present in the cache.
+ *
+ * NOTE: It is recommended that has() is only to be used for cache warming type purposes
+ * and not to be used within your live applications operations for get/set, as this method
+ * is subject to a race condition where your has() will return true and immediately after,
+ * another script can remove it making the state of your app out of date.
+ *
+ * @param string $key The cache item key.
+ *
+ * @return bool
+ *
+ * @throws \Psr\SimpleCache\InvalidArgumentException
+ * MUST be thrown if the $key string is not a legal value.
+ */
+ public function has($key);
+}
diff --git a/src/wp-includes/php-ai-client/third-party/Psr/SimpleCache/InvalidArgumentException.php b/src/wp-includes/php-ai-client/third-party/Psr/SimpleCache/InvalidArgumentException.php
new file mode 100644
index 0000000000000..7333cb827d27f
--- /dev/null
+++ b/src/wp-includes/php-ai-client/third-party/Psr/SimpleCache/InvalidArgumentException.php
@@ -0,0 +1,13 @@
+ /dev/null; then
+ echo "Error: '$1' is required but not found in PATH."
+ exit 1
+ fi
+}
+
+check_command php
+check_command composer
+check_command git
+
+# Verify we're running from the repo root.
+if [ ! -f "wp-cli.yml" ] && [ ! -f "wp-config-sample.php" ] && [ ! -d "src/wp-includes" ]; then
+ echo "Error: This script must be run from the WordPress development repository root."
+ exit 1
+fi
+
+echo "==> Starting php-ai-client installer..."
+
+# ---------------------------------------------------------------------------
+# Temp directory (cleaned on exit)
+# ---------------------------------------------------------------------------
+
+TEMP_DIR="$(mktemp -d)"
+trap 'rm -rf "$TEMP_DIR"' EXIT
+
+echo "==> Using temp directory: $TEMP_DIR"
+
+# ---------------------------------------------------------------------------
+# Fetch package
+# ---------------------------------------------------------------------------
+
+if [ -n "$BRANCH" ]; then
+ echo "==> Cloning branch '$BRANCH' from $GITHUB_REPO..."
+ git clone --depth 1 --branch "$BRANCH" "$GITHUB_REPO" "$TEMP_DIR/package"
+ echo "==> Installing Composer dependencies..."
+ composer install --no-dev --no-interaction --working-dir="$TEMP_DIR/package"
+ VENDOR_DIR="$TEMP_DIR/package/vendor"
+ CLIENT_SRC="$TEMP_DIR/package/src"
+else
+ echo "==> Fetching version '$VERSION' via Composer..."
+ mkdir -p "$TEMP_DIR/package"
+ composer init --no-interaction --name="temp/installer" --working-dir="$TEMP_DIR/package"
+ composer require "wordpress/php-ai-client:${VERSION}" --no-dev --no-interaction --working-dir="$TEMP_DIR/package"
+ VENDOR_DIR="$TEMP_DIR/package/vendor"
+ CLIENT_SRC="$VENDOR_DIR/wordpress/php-ai-client/src"
+fi
+
+if [ ! -d "$VENDOR_DIR" ]; then
+ echo "Error: vendor directory not found at $VENDOR_DIR"
+ exit 1
+fi
+
+echo "==> Package fetched successfully."
+
+# ---------------------------------------------------------------------------
+# Clean target directory
+# ---------------------------------------------------------------------------
+
+if [ -d "$TARGET_DIR" ]; then
+ echo "==> Removing existing $TARGET_DIR..."
+ rm -rf "$TARGET_DIR"
+fi
+
+# ---------------------------------------------------------------------------
+# Scope dependencies with PHP-Scoper
+# ---------------------------------------------------------------------------
+
+SCOPER_PHAR="$TEMP_DIR/php-scoper.phar"
+
+echo "==> Downloading PHP-Scoper ${SCOPER_VERSION}..."
+curl -fsSL "$SCOPER_URL" -o "$SCOPER_PHAR"
+chmod +x "$SCOPER_PHAR"
+
+# Copy scoper config into temp dir.
+cp "$SCRIPT_DIR/scoper.inc.php" "$TEMP_DIR/scoper.inc.php"
+
+SCOPED_DIR="$TEMP_DIR/scoped"
+
+echo "==> Running PHP-Scoper..."
+php "$SCOPER_PHAR" add-prefix \
+ --working-dir="$TEMP_DIR/package" \
+ --config="$TEMP_DIR/scoper.inc.php" \
+ --output-dir="$SCOPED_DIR" \
+ --force \
+ --no-interaction
+
+echo "==> Scoping complete."
+
+# ---------------------------------------------------------------------------
+# Reorganize scoped output into namespace-based layout
+# ---------------------------------------------------------------------------
+
+THIRD_PARTY_DIR="$TEMP_DIR/third-party"
+
+echo "==> Reorganizing dependencies..."
+php "$SCRIPT_DIR/reorganize.php" \
+ "$VENDOR_DIR/composer/installed.json" \
+ "$SCOPED_DIR/vendor" \
+ "$THIRD_PARTY_DIR"
+
+echo "==> Reorganization complete."
+
+# ---------------------------------------------------------------------------
+# Copy files to target
+# ---------------------------------------------------------------------------
+
+echo "==> Copying files to $TARGET_DIR..."
+
+mkdir -p "$TARGET_DIR/src"
+mkdir -p "$TARGET_DIR/third-party"
+
+# Copy scoped AI client source.
+# If installed via branch, scoped source is at scoped/src/.
+# If installed via version, scoped source is at scoped/vendor/wordpress/php-ai-client/src/.
+if [ -n "$BRANCH" ]; then
+ cp -R "$SCOPED_DIR/src/." "$TARGET_DIR/src/"
+else
+ cp -R "$SCOPED_DIR/vendor/wordpress/php-ai-client/src/." "$TARGET_DIR/src/"
+fi
+
+# Copy reorganized third-party dependencies.
+cp -R "$THIRD_PARTY_DIR/." "$TARGET_DIR/third-party/"
+
+# ---------------------------------------------------------------------------
+# Generate autoload.php
+# ---------------------------------------------------------------------------
+
+echo "==> Generating autoload.php..."
+
+cat > "$TARGET_DIR/autoload.php" << 'AUTOLOAD_PHP'
+ 16,
+ 'Psr\\Http\\Message\\' => 17,
+ 'Psr\\EventDispatcher\\' => 21,
+ 'Psr\\SimpleCache\\' => 16,
+ );
+
+ $base_dir = __DIR__;
+
+ // 1. WordPress\AiClient\* → src/
+ if ( 0 === strncmp( $class_name, $client_prefix, $client_prefix_len ) ) {
+ $relative_class = substr( $class_name, $client_prefix_len );
+ $file = $base_dir . '/src/' . str_replace( '\\', '/', $relative_class ) . '.php';
+ if ( file_exists( $file ) ) {
+ require $file;
+ }
+ return;
+ }
+
+ // 2. WordPress\AiClientDependencies\* → third-party/ (strip prefix).
+ if ( 0 === strncmp( $class_name, $scoped_prefix, $scoped_prefix_len ) ) {
+ $relative_class = substr( $class_name, $scoped_prefix_len );
+ $file = $base_dir . '/third-party/' . str_replace( '\\', '/', $relative_class ) . '.php';
+ if ( file_exists( $file ) ) {
+ require $file;
+ }
+ return;
+ }
+
+ // 3. Psr\* interfaces → third-party/Psr/...
+ foreach ( $psr_prefixes as $prefix => $prefix_len ) {
+ if ( 0 === strncmp( $class_name, $prefix, $prefix_len ) ) {
+ $relative_class = substr( $class_name, 4 ); // Strip 'Psr\' prefix, keep sub-namespace.
+ $file = $base_dir . '/third-party/Psr/' . str_replace( '\\', '/', $relative_class ) . '.php';
+ if ( file_exists( $file ) ) {
+ require $file;
+ }
+ return;
+ }
+ }
+ }
+);
+AUTOLOAD_PHP
+
+echo "==> autoload.php generated."
+
+# ---------------------------------------------------------------------------
+# Validate output
+# ---------------------------------------------------------------------------
+
+echo "==> Validating output..."
+
+ERRORS=0
+
+# Check key directories exist.
+for dir in "$TARGET_DIR/src" "$TARGET_DIR/third-party"; do
+ if [ ! -d "$dir" ]; then
+ echo "Error: Expected directory not found: $dir"
+ ERRORS=$((ERRORS + 1))
+ fi
+done
+
+# Check autoloader exists and has valid syntax.
+if [ ! -f "$TARGET_DIR/autoload.php" ]; then
+ echo "Error: autoload.php not found."
+ ERRORS=$((ERRORS + 1))
+else
+ if ! php -l "$TARGET_DIR/autoload.php" > /dev/null 2>&1; then
+ echo "Error: autoload.php has syntax errors."
+ php -l "$TARGET_DIR/autoload.php"
+ ERRORS=$((ERRORS + 1))
+ fi
+fi
+
+# Check that AiClient.php exists in source.
+if [ ! -f "$TARGET_DIR/src/AiClient.php" ]; then
+ echo "Warning: src/AiClient.php not found. The package structure may differ."
+fi
+
+# Check that Http dependencies are scoped.
+if [ -d "$TARGET_DIR/third-party/Http" ]; then
+ SCOPED_COUNT=$(grep -rl "namespace WordPress\\\\AiClientDependencies\\\\Http" "$TARGET_DIR/third-party/Http/" 2>/dev/null | wc -l | tr -d ' ')
+ if [ "$SCOPED_COUNT" -eq 0 ]; then
+ echo "Warning: No scoped Http\\* namespaces found in third-party/Http/."
+ else
+ echo " Found $SCOPED_COUNT scoped Http\\* files."
+ fi
+fi
+
+# Check that Psr interfaces are NOT scoped.
+if [ -d "$TARGET_DIR/third-party/Psr" ]; then
+ UNSCOPED_PSR=$(grep -rL "namespace WordPress\\\\AiClientDependencies" "$TARGET_DIR/third-party/Psr/" 2>/dev/null | wc -l | tr -d ' ')
+ echo " Found $UNSCOPED_PSR unscoped Psr\\* files."
+fi
+
+if [ "$ERRORS" -gt 0 ]; then
+ echo "Error: Validation failed with $ERRORS error(s)."
+ exit 1
+fi
+
+echo "==> Validation passed."
+echo "==> php-ai-client bundled successfully at $TARGET_DIR"
+echo ""
+echo "Next steps:"
+echo " 1. Verify: ls -R $TARGET_DIR"
+echo " 2. Test: php -r \"require '$TARGET_DIR/autoload.php'; var_dump(class_exists('WordPress\\\\AiClient\\\\AiClient'));\""
+echo " 3. Lint: composer lint:errors"
diff --git a/tools/php-ai-client/reorganize.php b/tools/php-ai-client/reorganize.php
new file mode 100644
index 0000000000000..026a95670a1c3
--- /dev/null
+++ b/tools/php-ai-client/reorganize.php
@@ -0,0 +1,176 @@
+
+ *
+ * @package WordPress
+ */
+
+if ( $argc < 4 ) {
+ fwrite( STDERR, "Usage: php reorganize.php \n" );
+ exit( 1 );
+}
+
+$installed_json_path = $argv[1];
+$scoped_vendor_dir = rtrim( $argv[2], '/' );
+$output_dir = rtrim( $argv[3], '/' );
+
+if ( ! file_exists( $installed_json_path ) ) {
+ fwrite( STDERR, "Error: installed.json not found at: $installed_json_path\n" );
+ exit( 1 );
+}
+
+if ( ! is_dir( $scoped_vendor_dir ) ) {
+ fwrite( STDERR, "Error: Scoped vendor directory not found at: $scoped_vendor_dir\n" );
+ exit( 1 );
+}
+
+// ---------------------------------------------------------------------------
+// Parse installed.json (handles Composer v1 and v2 formats).
+// ---------------------------------------------------------------------------
+
+$installed_data = json_decode( file_get_contents( $installed_json_path ), true );
+
+if ( null === $installed_data ) {
+ fwrite( STDERR, "Error: Failed to parse installed.json.\n" );
+ exit( 1 );
+}
+
+// Composer v2 wraps packages in a "packages" key; v1 is a flat array.
+if ( isset( $installed_data['packages'] ) && is_array( $installed_data['packages'] ) ) {
+ $packages = $installed_data['packages'];
+} elseif ( isset( $installed_data[0] ) ) {
+ $packages = $installed_data;
+} else {
+ fwrite( STDERR, "Error: Unrecognized installed.json format.\n" );
+ exit( 1 );
+}
+
+// ---------------------------------------------------------------------------
+// Process each dependency package.
+// ---------------------------------------------------------------------------
+
+$files_autoload = array();
+
+foreach ( $packages as $package ) {
+ $name = $package['name'] ?? '';
+
+ // Skip the AI client package itself.
+ if ( 'wordpress/php-ai-client' === $name ) {
+ continue;
+ }
+
+ // Get PSR-4 autoload mappings.
+ $psr4 = $package['autoload']['psr-4'] ?? array();
+
+ if ( empty( $psr4 ) ) {
+ // Check for PSR-0 as fallback.
+ $psr0 = $package['autoload']['psr-0'] ?? array();
+ if ( ! empty( $psr0 ) ) {
+ fwrite( STDERR, "Warning: Package '$name' uses PSR-0 autoloading (not fully supported). Skipping.\n" );
+ }
+ // Still check for files autoload below.
+ }
+
+ // Collect "files" autoload entries for future use.
+ $files = $package['autoload']['files'] ?? array();
+ if ( ! empty( $files ) ) {
+ foreach ( $files as $file ) {
+ $files_autoload[] = array(
+ 'package' => $name,
+ 'file' => $file,
+ );
+ }
+ }
+
+ // Process PSR-4 mappings.
+ foreach ( $psr4 as $namespace_prefix => $source_dirs ) {
+ // Normalize source_dirs to array.
+ if ( ! is_array( $source_dirs ) ) {
+ $source_dirs = array( $source_dirs );
+ }
+
+ // Convert namespace prefix to directory path.
+ // e.g., "Http\\Client\\" → "Http/Client"
+ $namespace_path = rtrim( str_replace( '\\', '/', $namespace_prefix ), '/' );
+
+ // Determine the source directory in the scoped vendor output.
+ // Composer packages are at vendor/{package-name}/{source-dir}/.
+ foreach ( $source_dirs as $source_dir ) {
+ $source_dir = rtrim( $source_dir, '/' );
+
+ // Build the source path in the scoped vendor directory.
+ $source_path = $scoped_vendor_dir . '/' . $name;
+ if ( '' !== $source_dir ) {
+ $source_path .= '/' . $source_dir;
+ }
+
+ if ( ! is_dir( $source_path ) ) {
+ fwrite( STDERR, "Warning: Source directory not found for '$name' at: $source_path\n" );
+ continue;
+ }
+
+ // Build the target path.
+ $target_path = $output_dir . '/' . $namespace_path;
+
+ // Create target directory.
+ if ( ! is_dir( $target_path ) ) {
+ mkdir( $target_path, 0755, true );
+ }
+
+ // Copy files recursively.
+ copy_directory( $source_path, $target_path );
+
+ echo " Copied: $name ($namespace_prefix) → $namespace_path\n";
+ }
+ }
+}
+
+if ( ! empty( $files_autoload ) ) {
+ fwrite( STDERR, "\nNote: The following packages have 'files' autoload entries that may need manual handling:\n" );
+ foreach ( $files_autoload as $entry ) {
+ fwrite( STDERR, " - {$entry['package']}: {$entry['file']}\n" );
+ }
+}
+
+echo "\nReorganization complete.\n";
+
+// ---------------------------------------------------------------------------
+// Helper functions.
+// ---------------------------------------------------------------------------
+
+/**
+ * Recursively copy a directory.
+ *
+ * @param string $source Source directory path.
+ * @param string $dest Destination directory path.
+ */
+function copy_directory( string $source, string $dest ): void {
+ $iterator = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator( $source, RecursiveDirectoryIterator::SKIP_DOTS ),
+ RecursiveIteratorIterator::SELF_FIRST
+ );
+
+ foreach ( $iterator as $item ) {
+ $target = $dest . '/' . $iterator->getSubPathname();
+
+ if ( $item->isDir() ) {
+ if ( ! is_dir( $target ) ) {
+ mkdir( $target, 0755, true );
+ }
+ } else {
+ // Ensure parent directory exists.
+ $parent = dirname( $target );
+ if ( ! is_dir( $parent ) ) {
+ mkdir( $parent, 0755, true );
+ }
+ copy( $item->getPathname(), $target );
+ }
+ }
+}
diff --git a/tools/php-ai-client/scoper.inc.php b/tools/php-ai-client/scoper.inc.php
new file mode 100644
index 0000000000000..cbe0428a9909b
--- /dev/null
+++ b/tools/php-ai-client/scoper.inc.php
@@ -0,0 +1,123 @@
+ 'WordPress\\AiClientDependencies',
+
+ 'finders' => array(
+ // Include all PHP files in vendor (dependencies) so their namespaces get scoped.
+ Finder::create()
+ ->files()
+ ->ignoreVCS( true )
+ ->notName( '/LICENSE|.*\\.md|.*\\.dist|Makefile/' )
+ ->exclude( array( 'doc', 'test', 'test_old', 'tests', 'Tests', 'vendor-bin' ) )
+ ->in( 'vendor' ),
+
+ // Include the AI client source files so `use` statements referencing
+ // scoped dependency namespaces get updated. The AI client's own namespace
+ // is excluded below, so its `namespace` declarations stay unchanged.
+ Finder::create()
+ ->files()
+ ->ignoreVCS( true )
+ ->name( '*.php' )
+ ->in( 'src' ),
+ ),
+
+ 'exclude-namespaces' => array(
+ // The AI client's own namespace must not be scoped.
+ 'WordPress\\AiClient',
+
+ // PSR interfaces stay global for type compatibility with external implementations.
+ 'Psr\\Http\\Client',
+ 'Psr\\Http\\Message',
+ 'Psr\\EventDispatcher',
+ 'Psr\\SimpleCache',
+
+ // Composer's own namespace.
+ 'Composer',
+ ),
+
+ 'exclude-files' => array(),
+
+ 'exclude-constants' => array(
+ // Preserve WordPress-compatible constants.
+ '/^ABSPATH$/',
+ '/^WPINC$/',
+ ),
+
+ 'exclude-functions' => array(
+ // polyfills.php defines global functions guarded by function_exists().
+ 'str_starts_with',
+ 'str_ends_with',
+ 'str_contains',
+ 'array_is_list',
+ ),
+
+ 'patchers' => array(
+ /**
+ * Fix php-http/discovery hardcoded class name strings.
+ *
+ * Discovery probes for external HTTP implementations using hardcoded FQCN strings.
+ * These must NOT be prefixed because they reference packages outside our bundle
+ * (e.g., GuzzleHttp\Client, Nyholm\Psr7\Factory\Psr17Factory).
+ */
+ static function ( string $file_path, string $prefix, string $contents ): string {
+ // Only patch php-http/discovery files.
+ if ( false === strpos( $file_path, 'php-http/discovery' ) ) {
+ return $contents;
+ }
+
+ // External package namespaces that Discovery probes for.
+ // These must remain un-prefixed in hardcoded string references.
+ $external_namespaces = array(
+ 'GuzzleHttp',
+ 'Http\\Adapter',
+ 'Http\\Client\\Curl',
+ 'Http\\Client\\Socket',
+ 'Http\\Client\\Buzz',
+ 'Http\\Client\\React',
+ 'Buzz',
+ 'Nyholm',
+ 'Laminas',
+ 'Symfony\\Component\\HttpClient',
+ 'Phalcon\\Http',
+ 'Slim\\Psr7',
+ 'Kriswallsmith',
+ );
+
+ foreach ( $external_namespaces as $ns ) {
+ $escaped_ns = preg_quote( $ns, '/' );
+ $escaped_prefix = preg_quote( $prefix, '/' );
+
+ // Remove prefix from string literals containing these namespaces.
+ // Matches: 'WordPress\AiClientDependencies\GuzzleHttp\...' or "WordPress\AiClientDependencies\GuzzleHttp\..."
+ $contents = preg_replace(
+ '/([\'"])' . $escaped_prefix . '\\\\\\\\' . $escaped_ns . '/',
+ '$1' . $ns,
+ $contents
+ );
+
+ // Also handle double-backslash variants in string concatenation.
+ $contents = preg_replace(
+ '/([\'"])' . $escaped_prefix . '\\\\' . $escaped_ns . '/',
+ '$1' . $ns,
+ $contents
+ );
+ }
+
+ return $contents;
+ },
+ ),
+);