first commit
This commit is contained in:
BIN
wp-includes/php-ai-client/.DS_Store
vendored
Normal file
BIN
wp-includes/php-ai-client/.DS_Store
vendored
Normal file
Binary file not shown.
45
wp-includes/php-ai-client/autoload.php
Normal file
45
wp-includes/php-ai-client/autoload.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
);
|
||||
387
wp-includes/php-ai-client/src/AiClient.php
Normal file
387
wp-includes/php-ai-client/src/AiClient.php
Normal file
@@ -0,0 +1,387 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
221
wp-includes/php-ai-client/src/Builders/MessageBuilder.php
Normal file
221
wp-includes/php-ai-client/src/Builders/MessageBuilder.php
Normal file
@@ -0,0 +1,221 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
1466
wp-includes/php-ai-client/src/Builders/PromptBuilder.php
Normal file
1466
wp-includes/php-ai-client/src/Builders/PromptBuilder.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,128 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
349
wp-includes/php-ai-client/src/Common/AbstractEnum.php
Normal file
349
wp-includes/php-ai-client/src/Common/AbstractEnum.php
Normal file
@@ -0,0 +1,349 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?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
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?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
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?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
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
400
wp-includes/php-ai-client/src/Files/DTO/File.php
Normal file
400
wp-includes/php-ai-client/src/Files/DTO/File.php
Normal file
@@ -0,0 +1,400 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
31
wp-includes/php-ai-client/src/Files/Enums/FileTypeEnum.php
Normal file
31
wp-includes/php-ai-client/src/Files/Enums/FileTypeEnum.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?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';
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?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';
|
||||
}
|
||||
255
wp-includes/php-ai-client/src/Files/ValueObjects/MimeType.php
Normal file
255
wp-includes/php-ai-client/src/Files/ValueObjects/MimeType.php
Normal file
@@ -0,0 +1,255 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
173
wp-includes/php-ai-client/src/Messages/DTO/Message.php
Normal file
173
wp-includes/php-ai-client/src/Messages/DTO/Message.php
Normal file
@@ -0,0 +1,173 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
266
wp-includes/php-ai-client/src/Messages/DTO/MessagePart.php
Normal file
266
wp-includes/php-ai-client/src/Messages/DTO/MessagePart.php
Normal file
@@ -0,0 +1,266 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
wp-includes/php-ai-client/src/Messages/DTO/ModelMessage.php
Normal file
32
wp-includes/php-ai-client/src/Messages/DTO/ModelMessage.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
31
wp-includes/php-ai-client/src/Messages/DTO/UserMessage.php
Normal file
31
wp-includes/php-ai-client/src/Messages/DTO/UserMessage.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?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';
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?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';
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?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';
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?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';
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?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';
|
||||
}
|
||||
120
wp-includes/php-ai-client/src/Providers/AbstractProvider.php
Normal file
120
wp-includes/php-ai-client/src/Providers/AbstractProvider.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?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, '/');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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;
|
||||
}
|
||||
215
wp-includes/php-ai-client/src/Providers/DTO/ProviderMetadata.php
Normal file
215
wp-includes/php-ai-client/src/Providers/DTO/ProviderMetadata.php
Normal file
@@ -0,0 +1,215 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?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]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?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';
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?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';
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?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]];
|
||||
}
|
||||
}
|
||||
377
wp-includes/php-ai-client/src/Providers/Http/DTO/Request.php
Normal file
377
wp-includes/php-ai-client/src/Providers/Http/DTO/Request.php
Normal file
@@ -0,0 +1,377 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
<?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));
|
||||
}
|
||||
}
|
||||
}
|
||||
211
wp-includes/php-ai-client/src/Providers/Http/DTO/Response.php
Normal file
211
wp-includes/php-ai-client/src/Providers/Http/DTO/Response.php
Normal file
@@ -0,0 +1,211 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?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());
|
||||
}
|
||||
}
|
||||
267
wp-includes/php-ai-client/src/Providers/Http/HttpTransporter.php
Normal file
267
wp-includes/php-ai-client/src/Providers/Http/HttpTransporter.php
Normal file
@@ -0,0 +1,267 @@
|
||||
<?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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?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()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,882 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
<?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]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?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]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?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';
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
<?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'] : '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
520
wp-includes/php-ai-client/src/Providers/ProviderRegistry.php
Normal file
520
wp-includes/php-ai-client/src/Providers/ProviderRegistry.php
Normal file
@@ -0,0 +1,520 @@
|
||||
<?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}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?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;
|
||||
}
|
||||
117
wp-includes/php-ai-client/src/Results/DTO/Candidate.php
Normal file
117
wp-includes/php-ai-client/src/Results/DTO/Candidate.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
420
wp-includes/php-ai-client/src/Results/DTO/GenerativeAiResult.php
Normal file
420
wp-includes/php-ai-client/src/Results/DTO/GenerativeAiResult.php
Normal file
@@ -0,0 +1,420 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
144
wp-includes/php-ai-client/src/Results/DTO/TokenUsage.php
Normal file
144
wp-includes/php-ai-client/src/Results/DTO/TokenUsage.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?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';
|
||||
}
|
||||
128
wp-includes/php-ai-client/src/Tools/DTO/FunctionCall.php
Normal file
128
wp-includes/php-ai-client/src/Tools/DTO/FunctionCall.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user