Make WordPress Core

Changeset 61942


Ignore:
Timestamp:
03/11/2026 03:44:33 PM (2 weeks ago)
Author:
gziolo
Message:

AI: Update php-ai-client to 1.3.0.

Updates the bundled php-ai-client library to version 1.3.0 and aligns the WordPress-specific prompt builder (WP_AI_Client_Prompt_Builder) with the upstream changes.

Key changes in php-ai-client 1.3.0:

  • Adds video generation model interfaces.
  • Enhances PromptBuilder with additional configuration methods.
  • Improves TokenUsage and ProviderMetadata DTOs.
  • Updates MessagePart DTO.

See release: https://github.com/WordPress/php-ai-client/releases/tag/1.3.0

Developed in https://github.com/WordPress/wordpress-develop/pull/11219

Props flixos90.
See #64591.

Location:
trunk
Files:
4 added
9 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php

    r61787 r61942  
    1111use WordPress\AiClient\Files\DTO\File;
    1212use WordPress\AiClient\Files\Enums\FileTypeEnum;
     13use WordPress\AiClient\Files\Enums\MediaOrientationEnum;
    1314use WordPress\AiClient\Messages\DTO\Message;
    1415use WordPress\AiClient\Messages\DTO\MessagePart;
     
    6768 * @method self as_output_modalities(ModalityEnum ...$modalities) Sets the output modalities.
    6869 * @method self as_output_file_type(FileTypeEnum $fileType) Sets the output file type.
     70 * @method self as_output_media_orientation(MediaOrientationEnum $orientation) Sets the output media orientation.
     71 * @method self as_output_media_aspect_ratio(string $aspectRatio) Sets the output media aspect ratio.
     72 * @method self as_output_speech_voice(string $voice) Sets the output speech voice.
    6973 * @method self as_json_response(?array<string, mixed> $schema = null) Configures the prompt for JSON response output.
    7074 * @method bool|WP_Error is_supported(?CapabilityEnum $capability = null) Checks if the prompt is supported for the given capability.
     
    8185 * @method GenerativeAiResult|WP_Error generate_speech_result() Generates a speech result from the prompt.
    8286 * @method GenerativeAiResult|WP_Error convert_text_to_speech_result() Converts text to speech and returns the result.
     87 * @method GenerativeAiResult|WP_Error generate_video_result() Generates a video result from the prompt.
    8388 * @method string|WP_Error generate_text() Generates text from the prompt.
    8489 * @method list<string>|WP_Error generate_texts(?int $candidateCount = null) Generates multiple text candidates from the prompt.
     
    8994 * @method File|WP_Error generate_speech() Generates speech from the prompt.
    9095 * @method list<File>|WP_Error generate_speeches(?int $candidateCount = null) Generates multiple speech outputs from the prompt.
     96 * @method File|WP_Error generate_video() Generates a video from the prompt.
     97 * @method list<File>|WP_Error generate_videos(?int $candidateCount = null) Generates multiple videos from the prompt.
    9198 */
    9299class WP_AI_Client_Prompt_Builder {
     
    122129        'generate_speech_result'        => true,
    123130        'convert_text_to_speech_result' => true,
     131        'generate_video_result'         => true,
    124132        'generate_text'                 => true,
    125133        'generate_texts'                => true,
     
    130138        'generate_speech'               => true,
    131139        'generate_speeches'             => true,
     140        'generate_video'                => true,
     141        'generate_videos'               => true,
    132142    );
    133143
  • trunk/src/wp-includes/php-ai-client/src/AiClient.php

    r61776 r61942  
    8585     * @var string The version of the AI Client.
    8686     */
    87     public const VERSION = '1.2.1';
     87    public const VERSION = '1.3.0';
    8888    /**
    8989     * @var ProviderRegistry|null The default provider registry instance.
     
    314314        self::validateModelOrConfigParameter($modelOrConfig);
    315315        return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->generateSpeechResult();
     316    }
     317    /**
     318     * Generates a video using the traditional API approach.
     319     *
     320     * @since 1.3.0
     321     *
     322     * @param Prompt $prompt The prompt content.
     323     * @param ModelInterface|ModelConfig|null $modelOrConfig Optional specific model to use,
     324     *                                                        or model configuration for auto-discovery,
     325     *                                                        or null for defaults.
     326     * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default.
     327     * @return GenerativeAiResult The generation result.
     328     *
     329     * @throws \InvalidArgumentException If the prompt format is invalid.
     330     * @throws \RuntimeException If no suitable model is found.
     331     */
     332    public static function generateVideoResult($prompt, $modelOrConfig = null, ?ProviderRegistry $registry = null): GenerativeAiResult
     333    {
     334        self::validateModelOrConfigParameter($modelOrConfig);
     335        return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->generateVideoResult();
    316336    }
    317337    /**
  • trunk/src/wp-includes/php-ai-client/src/Builders/PromptBuilder.php

    r61700 r61942  
    1111use WordPress\AiClient\Files\DTO\File;
    1212use WordPress\AiClient\Files\Enums\FileTypeEnum;
     13use WordPress\AiClient\Files\Enums\MediaOrientationEnum;
    1314use WordPress\AiClient\Messages\DTO\Message;
    1415use WordPress\AiClient\Messages\DTO\MessagePart;
     
    2728use WordPress\AiClient\Providers\Models\TextGeneration\Contracts\TextGenerationModelInterface;
    2829use WordPress\AiClient\Providers\Models\TextToSpeechConversion\Contracts\TextToSpeechConversionModelInterface;
     30use WordPress\AiClient\Providers\Models\VideoGeneration\Contracts\VideoGenerationModelInterface;
    2931use WordPress\AiClient\Providers\ProviderRegistry;
    3032use WordPress\AiClient\Results\DTO\GenerativeAiResult;
     
    399401    public function usingStopSequences(string ...$stopSequences): self
    400402    {
    401         $this->modelConfig->setCustomOption('stopSequences', $stopSequences);
     403        $this->modelConfig->setStopSequences($stopSequences);
    402404        return $this;
    403405    }
     
    551553    {
    552554        $this->modelConfig->setOutputFileType($fileType);
     555        return $this;
     556    }
     557    /**
     558     * Sets the output media orientation.
     559     *
     560     * @since 1.3.0
     561     *
     562     * @param MediaOrientationEnum $orientation The output media orientation.
     563     * @return self
     564     */
     565    public function asOutputMediaOrientation(MediaOrientationEnum $orientation): self
     566    {
     567        $this->modelConfig->setOutputMediaOrientation($orientation);
     568        return $this;
     569    }
     570    /**
     571     * Sets the output media aspect ratio.
     572     *
     573     * If set, this supersedes the output media orientation, as it is a more
     574     * specific configuration.
     575     *
     576     * @since 1.3.0
     577     *
     578     * @param string $aspectRatio The aspect ratio (e.g. "16:9", "3:2").
     579     * @return self
     580     */
     581    public function asOutputMediaAspectRatio(string $aspectRatio): self
     582    {
     583        $this->modelConfig->setOutputMediaAspectRatio($aspectRatio);
     584        return $this;
     585    }
     586    /**
     587     * Sets the output speech voice.
     588     *
     589     * @since 1.3.0
     590     *
     591     * @param string $voice The output speech voice.
     592     * @return self
     593     */
     594    public function asOutputSpeechVoice(string $voice): self
     595    {
     596        $this->modelConfig->setOutputSpeechVoice($voice);
    553597        return $this;
    554598    }
     
    628672            return CapabilityEnum::speechGeneration();
    629673        }
     674        if ($model instanceof VideoGenerationModelInterface) {
     675            return CapabilityEnum::videoGeneration();
     676        }
    630677        // No supported interface found
    631678        return null;
     
    826873            return $model->generateSpeechResult($messages);
    827874        }
    828         // Video generation is not yet implemented
    829875        if ($capability->isVideoGeneration()) {
    830             throw new RuntimeException('Output modality "video" is not yet supported.');
     876            if (!$model instanceof VideoGenerationModelInterface) {
     877                throw new RuntimeException(sprintf('Model "%s" does not support video generation.', $model->metadata()->getId()));
     878            }
     879            return $model->generateVideoResult($messages);
    831880        }
    832881        // TODO: Add support for other capabilities when interfaces are available
     
    898947    }
    899948    /**
     949     * Generates a video result from the prompt.
     950     *
     951     * @since 1.3.0
     952     *
     953     * @return GenerativeAiResult The generated result containing video candidates.
     954     * @throws InvalidArgumentException If the prompt or model validation fails.
     955     * @throws RuntimeException If the model doesn't support video generation.
     956     */
     957    public function generateVideoResult(): GenerativeAiResult
     958    {
     959        // Include video in output modalities
     960        $this->includeOutputModalities(ModalityEnum::video());
     961        // Generate and return the result with video generation capability
     962        return $this->generateResult(CapabilityEnum::videoGeneration());
     963    }
     964    /**
    900965     * Generates text from the prompt.
    901966     *
     
    10151080        }
    10161081        return $this->generateSpeechResult()->toFiles();
     1082    }
     1083    /**
     1084     * Generates a video from the prompt.
     1085     *
     1086     * @since 1.3.0
     1087     *
     1088     * @return File The generated video file.
     1089     * @throws InvalidArgumentException If the prompt or model validation fails.
     1090     * @throws RuntimeException If no video is generated.
     1091     */
     1092    public function generateVideo(): File
     1093    {
     1094        return $this->generateVideoResult()->toFile();
     1095    }
     1096    /**
     1097     * Generates multiple videos from the prompt.
     1098     *
     1099     * @since 1.3.0
     1100     *
     1101     * @param int|null $candidateCount The number of videos to generate.
     1102     * @return list<File> The generated video files.
     1103     * @throws InvalidArgumentException If the prompt or model validation fails.
     1104     * @throws RuntimeException If no videos are generated.
     1105     */
     1106    public function generateVideos(?int $candidateCount = null): array
     1107    {
     1108        if ($candidateCount !== null) {
     1109            $this->usingCandidateCount($candidateCount);
     1110        }
     1111        return $this->generateVideoResult()->toFiles();
    10171112    }
    10181113    /**
  • trunk/src/wp-includes/php-ai-client/src/Messages/DTO/MessagePart.php

    r61700 r61942  
    2727 *     channel: string,
    2828 *     type: string,
     29 *     thoughtSignature?: string,
    2930 *     text?: string,
    3031 *     file?: FileArrayShape,
     
    3940    public const KEY_CHANNEL = 'channel';
    4041    public const KEY_TYPE = 'type';
     42    public const KEY_THOUGHT_SIGNATURE = 'thoughtSignature';
    4143    public const KEY_TEXT = 'text';
    4244    public const KEY_FILE = 'file';
     
    5254    private MessagePartTypeEnum $type;
    5355    /**
     56     * @var string|null Thought signature for extended thinking.
     57     */
     58    private ?string $thoughtSignature = null;
     59    /**
    5460     * @var string|null Text content (when type is TEXT).
    5561     */
     
    7480     * @param mixed $content The content of this message part.
    7581     * @param MessagePartChannelEnum|null $channel The channel this part belongs to. Defaults to CONTENT.
     82     * @param string|null $thoughtSignature Optional thought signature for extended thinking.
    7683     * @throws InvalidArgumentException If an unsupported content type is provided.
    7784     */
    78     public function __construct($content, ?MessagePartChannelEnum $channel = null)
     85    public function __construct($content, ?MessagePartChannelEnum $channel = null, ?string $thoughtSignature = null)
    7986    {
    8087        $this->channel = $channel ?? MessagePartChannelEnum::content();
     88        $this->thoughtSignature = $thoughtSignature;
    8189        if (is_string($content)) {
    8290            $this->type = MessagePartTypeEnum::text();
     
    119127    }
    120128    /**
     129     * Gets the thought signature.
     130     *
     131     * @since 1.3.0
     132     *
     133     * @return string|null The thought signature or null if not set.
     134     */
     135    public function getThoughtSignature(): ?string
     136    {
     137        return $this->thoughtSignature;
     138    }
     139    /**
    121140     * Gets the text content.
    122141     *
     
    170189    {
    171190        $channelSchema = ['type' => 'string', 'enum' => MessagePartChannelEnum::getValues(), 'description' => 'The channel this message part belongs to.'];
    172         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.']], '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()], '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()], '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()], 'required' => [self::KEY_TYPE, self::KEY_FUNCTION_RESPONSE], 'additionalProperties' => \false]]];
     191        $thoughtSignatureSchema = ['type' => 'string', 'description' => 'Thought signature for extended thinking.'];
     192        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]]];
    173193    }
    174194    /**
     
    193213            throw new RuntimeException('MessagePart requires one of: text, file, functionCall, or functionResponse. ' . 'This should not be a possible condition.');
    194214        }
     215        if ($this->thoughtSignature !== null) {
     216            $data[self::KEY_THOUGHT_SIGNATURE] = $this->thoughtSignature;
     217        }
    195218        return $data;
    196219    }
     
    207230            $channel = null;
    208231        }
     232        $thoughtSignature = $array[self::KEY_THOUGHT_SIGNATURE] ?? null;
    209233        // Check which properties are set to determine how to construct the MessagePart
    210234        if (isset($array[self::KEY_TEXT])) {
    211             return new self($array[self::KEY_TEXT], $channel);
     235            return new self($array[self::KEY_TEXT], $channel, $thoughtSignature);
    212236        } elseif (isset($array[self::KEY_FILE])) {
    213             return new self(File::fromArray($array[self::KEY_FILE]), $channel);
     237            return new self(File::fromArray($array[self::KEY_FILE]), $channel, $thoughtSignature);
    214238        } elseif (isset($array[self::KEY_FUNCTION_CALL])) {
    215             return new self(FunctionCall::fromArray($array[self::KEY_FUNCTION_CALL]), $channel);
     239            return new self(FunctionCall::fromArray($array[self::KEY_FUNCTION_CALL]), $channel, $thoughtSignature);
    216240        } elseif (isset($array[self::KEY_FUNCTION_RESPONSE])) {
    217             return new self(FunctionResponse::fromArray($array[self::KEY_FUNCTION_RESPONSE]), $channel);
     241            return new self(FunctionResponse::fromArray($array[self::KEY_FUNCTION_RESPONSE]), $channel, $thoughtSignature);
    218242        } else {
    219243            throw new InvalidArgumentException('MessagePart requires one of: text, file, functionCall, or functionResponse.');
  • trunk/src/wp-includes/php-ai-client/src/Providers/DTO/ProviderMetadata.php

    r61776 r61942  
    1515 * @since 0.1.0
    1616 * @since 1.2.0 Added optional description property.
     17 * @since 1.3.0 Added optional logoPath property.
    1718 *
    1819 * @phpstan-type ProviderMetadataArrayShape array{
     
    2223 *     type: string,
    2324 *     credentialsUrl?: ?string,
    24  *     authenticationMethod?: ?string
     25 *     authenticationMethod?: ?string,
     26 *     logoPath?: ?string
    2527 * }
    2628 *
     
    3537    public const KEY_CREDENTIALS_URL = 'credentialsUrl';
    3638    public const KEY_AUTHENTICATION_METHOD = 'authenticationMethod';
     39    public const KEY_LOGO_PATH = 'logoPath';
    3740    /**
    3841     * @var string The provider's unique identifier.
     
    6063    protected ?RequestAuthenticationMethod $authenticationMethod;
    6164    /**
     65     * @var string|null The full path to the provider's logo image file.
     66     */
     67    protected ?string $logoPath;
     68    /**
    6269     * Constructor.
    6370     *
    6471     * @since 0.1.0
    6572     * @since 1.2.0 Added optional $description parameter.
     73     * @since 1.3.0 Added optional $logoPath parameter.
    6674     *
    6775     * @param string $id The provider's unique identifier.
     
    7179     * @param RequestAuthenticationMethod|null $authenticationMethod The authentication method.
    7280     * @param string|null $description The provider's description.
    73      */
    74     public function __construct(string $id, string $name, ProviderTypeEnum $type, ?string $credentialsUrl = null, ?RequestAuthenticationMethod $authenticationMethod = null, ?string $description = null)
     81     * @param string|null $logoPath The full path to the provider's logo image file.
     82     */
     83    public function __construct(string $id, string $name, ProviderTypeEnum $type, ?string $credentialsUrl = null, ?RequestAuthenticationMethod $authenticationMethod = null, ?string $description = null, ?string $logoPath = null)
    7584    {
    7685        $this->id = $id;
     
    8089        $this->credentialsUrl = $credentialsUrl;
    8190        $this->authenticationMethod = $authenticationMethod;
     91        $this->logoPath = $logoPath;
    8292    }
    8393    /**
     
    148158    }
    149159    /**
     160     * Gets the full path to the provider's logo image file.
     161     *
     162     * @since 1.3.0
     163     *
     164     * @return string|null The full path to the logo image file.
     165     */
     166    public function getLogoPath(): ?string
     167    {
     168        return $this->logoPath;
     169    }
     170    /**
    150171     * {@inheritDoc}
    151172     *
    152173     * @since 0.1.0
    153174     * @since 1.2.0 Added description to schema.
     175     * @since 1.3.0 Added logoPath to schema.
    154176     */
    155177    public static function getJsonSchema(): array
    156178    {
    157         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.']], 'required' => [self::KEY_ID, self::KEY_NAME, self::KEY_TYPE]];
     179        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]];
    158180    }
    159181    /**
     
    162184     * @since 0.1.0
    163185     * @since 1.2.0 Added description to output.
     186     * @since 1.3.0 Added logoPath to output.
    164187     *
    165188     * @return ProviderMetadataArrayShape
     
    167190    public function toArray(): array
    168191    {
    169         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];
     192        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];
    170193    }
    171194    /**
     
    174197     * @since 0.1.0
    175198     * @since 1.2.0 Added description support.
     199     * @since 1.3.0 Added logoPath support.
    176200     */
    177201    public static function fromArray(array $array): self
    178202    {
    179203        static::validateFromArrayData($array, [self::KEY_ID, self::KEY_NAME, self::KEY_TYPE]);
    180         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);
     204        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);
    181205    }
    182206}
  • trunk/src/wp-includes/php-ai-client/src/Providers/Http/Exception/ServerException.php

    r61700 r61942  
    3131    {
    3232        $statusCode = $response->getStatusCode();
    33         $statusTexts = [500 => 'Internal Server Error', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 507 => 'Insufficient Storage'];
     33        $statusTexts = [500 => 'Internal Server Error', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 507 => 'Insufficient Storage', 529 => 'Overloaded'];
    3434        if (isset($statusTexts[$statusCode])) {
    3535            $errorMessage = sprintf('%s (%d)', $statusTexts[$statusCode], $statusCode);
  • trunk/src/wp-includes/php-ai-client/src/Results/DTO/TokenUsage.php

    r61700 r61942  
    1111 * which is important for monitoring usage and costs.
    1212 *
     13 * Note that thought tokens are a subset of completion tokens, not additive.
     14 * In other words: completionTokens - thoughtTokens = tokens of actual output content.
     15 *
    1316 * @since 0.1.0
    1417 *
     
    1619 *     promptTokens: int,
    1720 *     completionTokens: int,
    18  *     totalTokens: int
     21 *     totalTokens: int,
     22 *     thoughtTokens?: int
    1923 * }
    2024 *
     
    2630    public const KEY_COMPLETION_TOKENS = 'completionTokens';
    2731    public const KEY_TOTAL_TOKENS = 'totalTokens';
     32    public const KEY_THOUGHT_TOKENS = 'thoughtTokens';
    2833    /**
    2934     * @var int Number of tokens in the prompt.
     
    3136    private int $promptTokens;
    3237    /**
    33      * @var int Number of tokens in the completion.
     38     * @var int Number of tokens in the completion, including any thought tokens.
    3439     */
    3540    private int $completionTokens;
     
    3944    private int $totalTokens;
    4045    /**
     46     * @var int|null Number of tokens used for thinking, as a subset of completion tokens.
     47     */
     48    private ?int $thoughtTokens;
     49    /**
    4150     * Constructor.
    4251     *
     
    4453     *
    4554     * @param int $promptTokens Number of tokens in the prompt.
    46      * @param int $completionTokens Number of tokens in the completion.
     55     * @param int $completionTokens Number of tokens in the completion, including any thought tokens.
    4756     * @param int $totalTokens Total number of tokens used.
     57     * @param int|null $thoughtTokens Number of tokens used for thinking, as a subset of completion tokens.
    4858     */
    49     public function __construct(int $promptTokens, int $completionTokens, int $totalTokens)
     59    public function __construct(int $promptTokens, int $completionTokens, int $totalTokens, ?int $thoughtTokens = null)
    5060    {
    5161        $this->promptTokens = $promptTokens;
    5262        $this->completionTokens = $completionTokens;
    5363        $this->totalTokens = $totalTokens;
     64        $this->thoughtTokens = $thoughtTokens;
    5465    }
    5566    /**
     
    6576    }
    6677    /**
    67      * Gets the number of completion tokens.
     78     * Gets the number of completion tokens, including any thought tokens.
    6879     *
    6980     * @since 0.1.0
     
    8798    }
    8899    /**
     100     * Gets the number of thought tokens, which is a subset of the completion token count.
     101     *
     102     * @since 1.3.0
     103     *
     104     * @return int|null The thought token count or null if not available.
     105     */
     106    public function getThoughtTokens(): ?int
     107    {
     108        return $this->thoughtTokens;
     109    }
     110    /**
    89111     * {@inheritDoc}
    90112     *
     
    93115    public static function getJsonSchema(): array
    94116    {
    95         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.'], self::KEY_TOTAL_TOKENS => ['type' => 'integer', 'description' => 'Total number of tokens used.']], 'required' => [self::KEY_PROMPT_TOKENS, self::KEY_COMPLETION_TOKENS, self::KEY_TOTAL_TOKENS]];
     117        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]];
    96118    }
    97119    /**
     
    104126    public function toArray(): array
    105127    {
    106         return [self::KEY_PROMPT_TOKENS => $this->promptTokens, self::KEY_COMPLETION_TOKENS => $this->completionTokens, self::KEY_TOTAL_TOKENS => $this->totalTokens];
     128        $data = [self::KEY_PROMPT_TOKENS => $this->promptTokens, self::KEY_COMPLETION_TOKENS => $this->completionTokens, self::KEY_TOTAL_TOKENS => $this->totalTokens];
     129        if ($this->thoughtTokens !== null) {
     130            $data[self::KEY_THOUGHT_TOKENS] = $this->thoughtTokens;
     131        }
     132        return $data;
    107133    }
    108134    /**
     
    114140    {
    115141        static::validateFromArrayData($array, [self::KEY_PROMPT_TOKENS, self::KEY_COMPLETION_TOKENS, self::KEY_TOTAL_TOKENS]);
    116         return new self($array[self::KEY_PROMPT_TOKENS], $array[self::KEY_COMPLETION_TOKENS], $array[self::KEY_TOTAL_TOKENS]);
     142        return new self($array[self::KEY_PROMPT_TOKENS], $array[self::KEY_COMPLETION_TOKENS], $array[self::KEY_TOTAL_TOKENS], $array[self::KEY_THOUGHT_TOKENS] ?? null);
    117143    }
    118144}
  • trunk/tests/phpunit/includes/wp-ai-client-mock-model-creation-trait.php

    r61700 r61942  
    1919use WordPress\AiClient\Providers\Models\TextGeneration\Contracts\TextGenerationModelInterface;
    2020use WordPress\AiClient\Providers\Models\TextToSpeechConversion\Contracts\TextToSpeechConversionModelInterface;
     21use WordPress\AiClient\Providers\Models\VideoGeneration\Contracts\VideoGenerationModelInterface;
    2122use WordPress\AiClient\Results\DTO\Candidate;
    2223use WordPress\AiClient\Results\DTO\GenerativeAiResult;
     
    142143
    143144    /**
     145     * Creates a test model metadata instance for video generation.
     146     *
     147     * @param string $id   Optional model ID.
     148     * @param string $name Optional model name.
     149     * @return ModelMetadata
     150     */
     151    protected function create_test_video_model_metadata(
     152        string $id = 'test-video-model',
     153        string $name = 'Test Video Model'
     154    ): ModelMetadata {
     155        return new ModelMetadata(
     156            $id,
     157            $name,
     158            array( CapabilityEnum::videoGeneration() ),
     159            array()
     160        );
     161    }
     162
     163    /**
    144164     * Creates a mock text generation model using anonymous class.
    145165     *
     
    382402
    383403    /**
     404     * Creates a mock video generation model using anonymous class.
     405     *
     406     * @param GenerativeAiResult $result   The result to return from generation.
     407     * @param ModelMetadata|null $metadata Optional metadata.
     408     * @return ModelInterface&VideoGenerationModelInterface The mock model.
     409     */
     410    protected function create_mock_video_generation_model(
     411        GenerativeAiResult $result,
     412        ?ModelMetadata $metadata = null
     413    ): ModelInterface {
     414        $metadata = $metadata ?? $this->create_test_video_model_metadata();
     415
     416        $provider_metadata = new ProviderMetadata(
     417            'mock-provider',
     418            'Mock Provider',
     419            ProviderTypeEnum::cloud()
     420        );
     421
     422        return new class( $metadata, $provider_metadata, $result ) implements ModelInterface, VideoGenerationModelInterface {
     423
     424            private ModelMetadata $metadata;
     425            private ProviderMetadata $provider_metadata;
     426            private GenerativeAiResult $result;
     427            private ModelConfig $config;
     428
     429            public function __construct(
     430                ModelMetadata $metadata,
     431                ProviderMetadata $provider_metadata,
     432                GenerativeAiResult $result
     433            ) {
     434                $this->metadata          = $metadata;
     435                $this->provider_metadata = $provider_metadata;
     436                $this->result            = $result;
     437                $this->config            = new ModelConfig();
     438            }
     439
     440            public function metadata(): ModelMetadata {
     441                return $this->metadata;
     442            }
     443
     444            public function providerMetadata(): ProviderMetadata {
     445                return $this->provider_metadata;
     446            }
     447
     448            public function setConfig( ModelConfig $config ): void {
     449                $this->config = $config;
     450            }
     451
     452            public function getConfig(): ModelConfig {
     453                return $this->config;
     454            }
     455
     456            public function generateVideoResult( array $prompt ): GenerativeAiResult { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
     457                return $this->result;
     458            }
     459        };
     460    }
     461
     462    /**
    384463     * Creates a mock text generation model that throws an exception.
    385464     *
  • trunk/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php

    r61787 r61942  
    99use WordPress\AiClient\AiClient;
    1010use WordPress\AiClient\Files\DTO\File;
     11use WordPress\AiClient\Files\Enums\MediaOrientationEnum;
    1112use WordPress\AiClient\Messages\DTO\Message;
    1213use WordPress\AiClient\Messages\DTO\MessagePart;
     
    10301031
    10311032        /** @var ModelConfig $merged_config */
    1032         $merged_config  = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' );
     1033        $merged_config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' );
     1034
     1035        $this->assertEquals( array( 'STOP' ), $merged_config->getStopSequences() );
     1036
    10331037        $custom_options = $merged_config->getCustomOptions();
    1034 
    10351038        $this->assertArrayHasKey( 'stopSequences', $custom_options );
    1036         $this->assertIsArray( $custom_options['stopSequences'] );
    1037         $this->assertEquals( array( 'STOP' ), $custom_options['stopSequences'] );
     1039        $this->assertEquals( array( 'CONFIG_STOP' ), $custom_options['stopSequences'] );
    10381040        $this->assertArrayHasKey( 'otherOption', $custom_options );
    10391041        $this->assertEquals( 'value', $custom_options['otherOption'] );
     
    11541156        $config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' );
    11551157
    1156         $custom_options = $config->getCustomOptions();
    1157         $this->assertArrayHasKey( 'stopSequences', $custom_options );
    1158         $this->assertEquals( array( 'STOP', 'END', '###' ), $custom_options['stopSequences'] );
     1158        $this->assertEquals( array( 'STOP', 'END', '###' ), $config->getStopSequences() );
    11591159    }
    11601160
     
    15591559        $this->assertWPError( $result );
    15601560        $this->assertSame( 'prompt_builder_error', $result->get_error_code() );
    1561         $this->assertStringContainsString( 'Output modality "video" is not yet supported', $result->get_error_message() );
     1561        $this->assertStringContainsString( 'does not support video generation', $result->get_error_message() );
    15621562    }
    15631563
     
    20532053
    20542054    /**
     2055     * Tests generateVideo method.
     2056     *
     2057     * @ticket 64591
     2058     */
     2059    public function test_generate_video() {
     2060        $file         = new File( 'https://example.com/video.mp4', 'video/mp4' );
     2061        $message_part = new MessagePart( $file );
     2062        $message      = new Message( MessageRoleEnum::model(), array( $message_part ) );
     2063        $candidate    = new Candidate( $message, FinishReasonEnum::stop() );
     2064
     2065        $result = new GenerativeAiResult(
     2066            'test-result',
     2067            array( $candidate ),
     2068            new TokenUsage( 100, 50, 150 ),
     2069            $this->create_test_provider_metadata(),
     2070            $this->create_test_video_model_metadata()
     2071        );
     2072
     2073        $metadata = $this->createMock( ModelMetadata::class );
     2074        $metadata->method( 'getId' )->willReturn( 'test-model' );
     2075
     2076        $model = $this->create_mock_video_generation_model( $result, $metadata );
     2077
     2078        $builder = new WP_AI_Client_Prompt_Builder( $this->registry, 'Generate video' );
     2079        $builder->using_model( $model );
     2080
     2081        $video_file = $builder->generate_video();
     2082        $this->assertSame( $file, $video_file );
     2083    }
     2084
     2085    /**
     2086     * Tests generateVideos method.
     2087     *
     2088     * @ticket 64591
     2089     */
     2090    public function test_generate_videos() {
     2091        $files = array(
     2092            new File( 'https://example.com/video1.mp4', 'video/mp4' ),
     2093            new File( 'https://example.com/video2.mp4', 'video/mp4' ),
     2094        );
     2095
     2096        $candidates = array();
     2097        foreach ( $files as $file ) {
     2098            $candidates[] = new Candidate(
     2099                new Message( MessageRoleEnum::model(), array( new MessagePart( $file ) ) ),
     2100                FinishReasonEnum::stop()
     2101            );
     2102        }
     2103
     2104        $result = new GenerativeAiResult(
     2105            'test-result-id',
     2106            $candidates,
     2107            new TokenUsage( 100, 50, 150 ),
     2108            $this->create_test_provider_metadata(),
     2109            $this->create_test_video_model_metadata()
     2110        );
     2111
     2112        $metadata = $this->createMock( ModelMetadata::class );
     2113        $metadata->method( 'getId' )->willReturn( 'test-model' );
     2114
     2115        $model = $this->create_mock_video_generation_model( $result, $metadata );
     2116
     2117        $builder = new WP_AI_Client_Prompt_Builder( $this->registry, 'Generate videos' );
     2118        $builder->using_model( $model );
     2119
     2120        $video_files = $builder->generate_videos( 2 );
     2121
     2122        $this->assertCount( 2, $video_files );
     2123        $this->assertSame( $files[0], $video_files[0] );
     2124        $this->assertSame( $files[1], $video_files[1] );
     2125    }
     2126
     2127    /**
     2128     * Tests generateVideoResult method.
     2129     *
     2130     * @ticket 64591
     2131     */
     2132    public function test_generate_video_result() {
     2133        $result = new GenerativeAiResult(
     2134            'test-result',
     2135            array(
     2136                new Candidate(
     2137                    new ModelMessage( array( new MessagePart( new File( 'data:video/mp4;base64,AAAAAA==', 'video/mp4' ) ) ) ),
     2138                    FinishReasonEnum::stop()
     2139                ),
     2140            ),
     2141            new TokenUsage( 100, 50, 150 ),
     2142            $this->create_test_provider_metadata(),
     2143            $this->create_test_video_model_metadata()
     2144        );
     2145
     2146        $metadata = $this->createMock( ModelMetadata::class );
     2147        $metadata->method( 'getId' )->willReturn( 'test-model' );
     2148
     2149        $model = $this->create_mock_video_generation_model( $result, $metadata );
     2150
     2151        $builder = new WP_AI_Client_Prompt_Builder( $this->registry, 'Generate video' );
     2152        $builder->using_model( $model );
     2153
     2154        $actual_result = $builder->generate_video_result();
     2155        $this->assertSame( $result, $actual_result );
     2156
     2157        /** @var ModelConfig $config */
     2158        $config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' );
     2159
     2160        $modalities = $config->getOutputModalities();
     2161        $this->assertNotNull( $modalities );
     2162        $this->assertTrue( $modalities[0]->isVideo() );
     2163    }
     2164
     2165    /**
     2166     * Tests asOutputMediaOrientation method.
     2167     *
     2168     * @ticket 64591
     2169     */
     2170    public function test_as_output_media_orientation() {
     2171        $builder = new WP_AI_Client_Prompt_Builder( $this->registry );
     2172        $result  = $builder->as_output_media_orientation( MediaOrientationEnum::landscape() );
     2173
     2174        $this->assertSame( $builder, $result );
     2175
     2176        /** @var ModelConfig $config */
     2177        $config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' );
     2178
     2179        $this->assertTrue( $config->getOutputMediaOrientation()->isLandscape() );
     2180    }
     2181
     2182    /**
     2183     * Tests asOutputMediaAspectRatio method.
     2184     *
     2185     * @ticket 64591
     2186     */
     2187    public function test_as_output_media_aspect_ratio() {
     2188        $builder = new WP_AI_Client_Prompt_Builder( $this->registry );
     2189        $result  = $builder->as_output_media_aspect_ratio( '16:9' );
     2190
     2191        $this->assertSame( $builder, $result );
     2192
     2193        /** @var ModelConfig $config */
     2194        $config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' );
     2195
     2196        $this->assertEquals( '16:9', $config->getOutputMediaAspectRatio() );
     2197    }
     2198
     2199    /**
     2200     * Tests asOutputSpeechVoice method.
     2201     *
     2202     * @ticket 64591
     2203     */
     2204    public function test_as_output_speech_voice() {
     2205        $builder = new WP_AI_Client_Prompt_Builder( $this->registry );
     2206        $result  = $builder->as_output_speech_voice( 'alloy' );
     2207
     2208        $this->assertSame( $builder, $result );
     2209
     2210        /** @var ModelConfig $config */
     2211        $config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' );
     2212
     2213        $this->assertEquals( 'alloy', $config->getOutputSpeechVoice() );
     2214    }
     2215
     2216    /**
    20552217     * Tests using_abilities with ability name string.
    20562218     *
Note: See TracChangeset for help on using the changeset viewer.