Changeset 61942
- Timestamp:
- 03/11/2026 03:44:33 PM (2 weeks ago)
- Location:
- trunk
- Files:
-
- 4 added
- 9 edited
-
src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php (modified) (6 diffs)
-
src/wp-includes/php-ai-client/src/AiClient.php (modified) (2 diffs)
-
src/wp-includes/php-ai-client/src/Builders/PromptBuilder.php (modified) (8 diffs)
-
src/wp-includes/php-ai-client/src/Messages/DTO/MessagePart.php (modified) (8 diffs)
-
src/wp-includes/php-ai-client/src/Providers/DTO/ProviderMetadata.php (modified) (10 diffs)
-
src/wp-includes/php-ai-client/src/Providers/Http/Exception/ServerException.php (modified) (1 diff)
-
src/wp-includes/php-ai-client/src/Providers/Models/VideoGeneration (added)
-
src/wp-includes/php-ai-client/src/Providers/Models/VideoGeneration/Contracts (added)
-
src/wp-includes/php-ai-client/src/Providers/Models/VideoGeneration/Contracts/VideoGenerationModelInterface.php (added)
-
src/wp-includes/php-ai-client/src/Providers/Models/VideoGeneration/Contracts/VideoGenerationOperationModelInterface.php (added)
-
src/wp-includes/php-ai-client/src/Results/DTO/TokenUsage.php (modified) (11 diffs)
-
tests/phpunit/includes/wp-ai-client-mock-model-creation-trait.php (modified) (3 diffs)
-
tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php
r61787 r61942 11 11 use WordPress\AiClient\Files\DTO\File; 12 12 use WordPress\AiClient\Files\Enums\FileTypeEnum; 13 use WordPress\AiClient\Files\Enums\MediaOrientationEnum; 13 14 use WordPress\AiClient\Messages\DTO\Message; 14 15 use WordPress\AiClient\Messages\DTO\MessagePart; … … 67 68 * @method self as_output_modalities(ModalityEnum ...$modalities) Sets the output modalities. 68 69 * @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. 69 73 * @method self as_json_response(?array<string, mixed> $schema = null) Configures the prompt for JSON response output. 70 74 * @method bool|WP_Error is_supported(?CapabilityEnum $capability = null) Checks if the prompt is supported for the given capability. … … 81 85 * @method GenerativeAiResult|WP_Error generate_speech_result() Generates a speech result from the prompt. 82 86 * @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. 83 88 * @method string|WP_Error generate_text() Generates text from the prompt. 84 89 * @method list<string>|WP_Error generate_texts(?int $candidateCount = null) Generates multiple text candidates from the prompt. … … 89 94 * @method File|WP_Error generate_speech() Generates speech from the prompt. 90 95 * @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. 91 98 */ 92 99 class WP_AI_Client_Prompt_Builder { … … 122 129 'generate_speech_result' => true, 123 130 'convert_text_to_speech_result' => true, 131 'generate_video_result' => true, 124 132 'generate_text' => true, 125 133 'generate_texts' => true, … … 130 138 'generate_speech' => true, 131 139 'generate_speeches' => true, 140 'generate_video' => true, 141 'generate_videos' => true, 132 142 ); 133 143 -
trunk/src/wp-includes/php-ai-client/src/AiClient.php
r61776 r61942 85 85 * @var string The version of the AI Client. 86 86 */ 87 public const VERSION = '1. 2.1';87 public const VERSION = '1.3.0'; 88 88 /** 89 89 * @var ProviderRegistry|null The default provider registry instance. … … 314 314 self::validateModelOrConfigParameter($modelOrConfig); 315 315 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(); 316 336 } 317 337 /** -
trunk/src/wp-includes/php-ai-client/src/Builders/PromptBuilder.php
r61700 r61942 11 11 use WordPress\AiClient\Files\DTO\File; 12 12 use WordPress\AiClient\Files\Enums\FileTypeEnum; 13 use WordPress\AiClient\Files\Enums\MediaOrientationEnum; 13 14 use WordPress\AiClient\Messages\DTO\Message; 14 15 use WordPress\AiClient\Messages\DTO\MessagePart; … … 27 28 use WordPress\AiClient\Providers\Models\TextGeneration\Contracts\TextGenerationModelInterface; 28 29 use WordPress\AiClient\Providers\Models\TextToSpeechConversion\Contracts\TextToSpeechConversionModelInterface; 30 use WordPress\AiClient\Providers\Models\VideoGeneration\Contracts\VideoGenerationModelInterface; 29 31 use WordPress\AiClient\Providers\ProviderRegistry; 30 32 use WordPress\AiClient\Results\DTO\GenerativeAiResult; … … 399 401 public function usingStopSequences(string ...$stopSequences): self 400 402 { 401 $this->modelConfig->set CustomOption('stopSequences',$stopSequences);403 $this->modelConfig->setStopSequences($stopSequences); 402 404 return $this; 403 405 } … … 551 553 { 552 554 $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); 553 597 return $this; 554 598 } … … 628 672 return CapabilityEnum::speechGeneration(); 629 673 } 674 if ($model instanceof VideoGenerationModelInterface) { 675 return CapabilityEnum::videoGeneration(); 676 } 630 677 // No supported interface found 631 678 return null; … … 826 873 return $model->generateSpeechResult($messages); 827 874 } 828 // Video generation is not yet implemented829 875 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); 831 880 } 832 881 // TODO: Add support for other capabilities when interfaces are available … … 898 947 } 899 948 /** 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 /** 900 965 * Generates text from the prompt. 901 966 * … … 1015 1080 } 1016 1081 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(); 1017 1112 } 1018 1113 /** -
trunk/src/wp-includes/php-ai-client/src/Messages/DTO/MessagePart.php
r61700 r61942 27 27 * channel: string, 28 28 * type: string, 29 * thoughtSignature?: string, 29 30 * text?: string, 30 31 * file?: FileArrayShape, … … 39 40 public const KEY_CHANNEL = 'channel'; 40 41 public const KEY_TYPE = 'type'; 42 public const KEY_THOUGHT_SIGNATURE = 'thoughtSignature'; 41 43 public const KEY_TEXT = 'text'; 42 44 public const KEY_FILE = 'file'; … … 52 54 private MessagePartTypeEnum $type; 53 55 /** 56 * @var string|null Thought signature for extended thinking. 57 */ 58 private ?string $thoughtSignature = null; 59 /** 54 60 * @var string|null Text content (when type is TEXT). 55 61 */ … … 74 80 * @param mixed $content The content of this message part. 75 81 * @param MessagePartChannelEnum|null $channel The channel this part belongs to. Defaults to CONTENT. 82 * @param string|null $thoughtSignature Optional thought signature for extended thinking. 76 83 * @throws InvalidArgumentException If an unsupported content type is provided. 77 84 */ 78 public function __construct($content, ?MessagePartChannelEnum $channel = null )85 public function __construct($content, ?MessagePartChannelEnum $channel = null, ?string $thoughtSignature = null) 79 86 { 80 87 $this->channel = $channel ?? MessagePartChannelEnum::content(); 88 $this->thoughtSignature = $thoughtSignature; 81 89 if (is_string($content)) { 82 90 $this->type = MessagePartTypeEnum::text(); … … 119 127 } 120 128 /** 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 /** 121 140 * Gets the text content. 122 141 * … … 170 189 { 171 190 $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]]]; 173 193 } 174 194 /** … … 193 213 throw new RuntimeException('MessagePart requires one of: text, file, functionCall, or functionResponse. ' . 'This should not be a possible condition.'); 194 214 } 215 if ($this->thoughtSignature !== null) { 216 $data[self::KEY_THOUGHT_SIGNATURE] = $this->thoughtSignature; 217 } 195 218 return $data; 196 219 } … … 207 230 $channel = null; 208 231 } 232 $thoughtSignature = $array[self::KEY_THOUGHT_SIGNATURE] ?? null; 209 233 // Check which properties are set to determine how to construct the MessagePart 210 234 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); 212 236 } 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); 214 238 } 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); 216 240 } 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); 218 242 } else { 219 243 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 15 15 * @since 0.1.0 16 16 * @since 1.2.0 Added optional description property. 17 * @since 1.3.0 Added optional logoPath property. 17 18 * 18 19 * @phpstan-type ProviderMetadataArrayShape array{ … … 22 23 * type: string, 23 24 * credentialsUrl?: ?string, 24 * authenticationMethod?: ?string 25 * authenticationMethod?: ?string, 26 * logoPath?: ?string 25 27 * } 26 28 * … … 35 37 public const KEY_CREDENTIALS_URL = 'credentialsUrl'; 36 38 public const KEY_AUTHENTICATION_METHOD = 'authenticationMethod'; 39 public const KEY_LOGO_PATH = 'logoPath'; 37 40 /** 38 41 * @var string The provider's unique identifier. … … 60 63 protected ?RequestAuthenticationMethod $authenticationMethod; 61 64 /** 65 * @var string|null The full path to the provider's logo image file. 66 */ 67 protected ?string $logoPath; 68 /** 62 69 * Constructor. 63 70 * 64 71 * @since 0.1.0 65 72 * @since 1.2.0 Added optional $description parameter. 73 * @since 1.3.0 Added optional $logoPath parameter. 66 74 * 67 75 * @param string $id The provider's unique identifier. … … 71 79 * @param RequestAuthenticationMethod|null $authenticationMethod The authentication method. 72 80 * @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) 75 84 { 76 85 $this->id = $id; … … 80 89 $this->credentialsUrl = $credentialsUrl; 81 90 $this->authenticationMethod = $authenticationMethod; 91 $this->logoPath = $logoPath; 82 92 } 83 93 /** … … 148 158 } 149 159 /** 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 /** 150 171 * {@inheritDoc} 151 172 * 152 173 * @since 0.1.0 153 174 * @since 1.2.0 Added description to schema. 175 * @since 1.3.0 Added logoPath to schema. 154 176 */ 155 177 public static function getJsonSchema(): array 156 178 { 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]]; 158 180 } 159 181 /** … … 162 184 * @since 0.1.0 163 185 * @since 1.2.0 Added description to output. 186 * @since 1.3.0 Added logoPath to output. 164 187 * 165 188 * @return ProviderMetadataArrayShape … … 167 190 public function toArray(): array 168 191 { 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]; 170 193 } 171 194 /** … … 174 197 * @since 0.1.0 175 198 * @since 1.2.0 Added description support. 199 * @since 1.3.0 Added logoPath support. 176 200 */ 177 201 public static function fromArray(array $array): self 178 202 { 179 203 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); 181 205 } 182 206 } -
trunk/src/wp-includes/php-ai-client/src/Providers/Http/Exception/ServerException.php
r61700 r61942 31 31 { 32 32 $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']; 34 34 if (isset($statusTexts[$statusCode])) { 35 35 $errorMessage = sprintf('%s (%d)', $statusTexts[$statusCode], $statusCode); -
trunk/src/wp-includes/php-ai-client/src/Results/DTO/TokenUsage.php
r61700 r61942 11 11 * which is important for monitoring usage and costs. 12 12 * 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 * 13 16 * @since 0.1.0 14 17 * … … 16 19 * promptTokens: int, 17 20 * completionTokens: int, 18 * totalTokens: int 21 * totalTokens: int, 22 * thoughtTokens?: int 19 23 * } 20 24 * … … 26 30 public const KEY_COMPLETION_TOKENS = 'completionTokens'; 27 31 public const KEY_TOTAL_TOKENS = 'totalTokens'; 32 public const KEY_THOUGHT_TOKENS = 'thoughtTokens'; 28 33 /** 29 34 * @var int Number of tokens in the prompt. … … 31 36 private int $promptTokens; 32 37 /** 33 * @var int Number of tokens in the completion .38 * @var int Number of tokens in the completion, including any thought tokens. 34 39 */ 35 40 private int $completionTokens; … … 39 44 private int $totalTokens; 40 45 /** 46 * @var int|null Number of tokens used for thinking, as a subset of completion tokens. 47 */ 48 private ?int $thoughtTokens; 49 /** 41 50 * Constructor. 42 51 * … … 44 53 * 45 54 * @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. 47 56 * @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. 48 58 */ 49 public function __construct(int $promptTokens, int $completionTokens, int $totalTokens )59 public function __construct(int $promptTokens, int $completionTokens, int $totalTokens, ?int $thoughtTokens = null) 50 60 { 51 61 $this->promptTokens = $promptTokens; 52 62 $this->completionTokens = $completionTokens; 53 63 $this->totalTokens = $totalTokens; 64 $this->thoughtTokens = $thoughtTokens; 54 65 } 55 66 /** … … 65 76 } 66 77 /** 67 * Gets the number of completion tokens .78 * Gets the number of completion tokens, including any thought tokens. 68 79 * 69 80 * @since 0.1.0 … … 87 98 } 88 99 /** 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 /** 89 111 * {@inheritDoc} 90 112 * … … 93 115 public static function getJsonSchema(): array 94 116 { 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]]; 96 118 } 97 119 /** … … 104 126 public function toArray(): array 105 127 { 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; 107 133 } 108 134 /** … … 114 140 { 115 141 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); 117 143 } 118 144 } -
trunk/tests/phpunit/includes/wp-ai-client-mock-model-creation-trait.php
r61700 r61942 19 19 use WordPress\AiClient\Providers\Models\TextGeneration\Contracts\TextGenerationModelInterface; 20 20 use WordPress\AiClient\Providers\Models\TextToSpeechConversion\Contracts\TextToSpeechConversionModelInterface; 21 use WordPress\AiClient\Providers\Models\VideoGeneration\Contracts\VideoGenerationModelInterface; 21 22 use WordPress\AiClient\Results\DTO\Candidate; 22 23 use WordPress\AiClient\Results\DTO\GenerativeAiResult; … … 142 143 143 144 /** 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 /** 144 164 * Creates a mock text generation model using anonymous class. 145 165 * … … 382 402 383 403 /** 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 /** 384 463 * Creates a mock text generation model that throws an exception. 385 464 * -
trunk/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php
r61787 r61942 9 9 use WordPress\AiClient\AiClient; 10 10 use WordPress\AiClient\Files\DTO\File; 11 use WordPress\AiClient\Files\Enums\MediaOrientationEnum; 11 12 use WordPress\AiClient\Messages\DTO\Message; 12 13 use WordPress\AiClient\Messages\DTO\MessagePart; … … 1030 1031 1031 1032 /** @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 1033 1037 $custom_options = $merged_config->getCustomOptions(); 1034 1035 1038 $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'] ); 1038 1040 $this->assertArrayHasKey( 'otherOption', $custom_options ); 1039 1041 $this->assertEquals( 'value', $custom_options['otherOption'] ); … … 1154 1156 $config = $this->get_wrapped_prompt_builder_property_value( $builder, 'modelConfig' ); 1155 1157 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() ); 1159 1159 } 1160 1160 … … 1559 1559 $this->assertWPError( $result ); 1560 1560 $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() ); 1562 1562 } 1563 1563 … … 2053 2053 2054 2054 /** 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 /** 2055 2217 * Tests using_abilities with ability name string. 2056 2218 *
Note: See TracChangeset
for help on using the changeset viewer.