| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 3 declare (strict_types=1); 4 namespace WordPress\AiClient\Providers\OpenAiCompatibleImplementation; 5 6 use WordPress\AiClient\Common\Exception\InvalidArgumentException; 7 use WordPress\AiClient\Common\Exception\RuntimeException; 8 use WordPress\AiClient\Files\DTO\File; 9 use WordPress\AiClient\Files\Enums\MediaOrientationEnum; 10 use WordPress\AiClient\Messages\DTO\Message; 11 use WordPress\AiClient\Messages\DTO\MessagePart; 12 use WordPress\AiClient\Messages\Enums\MessageRoleEnum; 13 use WordPress\AiClient\Providers\ApiBasedImplementation\AbstractApiBasedModel; 14 use WordPress\AiClient\Providers\Http\DTO\Request; 15 use WordPress\AiClient\Providers\Http\DTO\Response; 16 use WordPress\AiClient\Providers\Http\Enums\HttpMethodEnum; 17 use WordPress\AiClient\Providers\Http\Exception\ResponseException; 18 use WordPress\AiClient\Providers\Http\Util\ResponseUtil; 19 use WordPress\AiClient\Providers\Models\ImageGeneration\Contracts\ImageGenerationModelInterface; 20 use WordPress\AiClient\Results\DTO\Candidate; 21 use WordPress\AiClient\Results\DTO\GenerativeAiResult; 22 use WordPress\AiClient\Results\DTO\TokenUsage; 23 use WordPress\AiClient\Results\Enums\FinishReasonEnum; 24 /** 25 * Base class for an image generation model for providers that implement OpenAI's API format. 26 * 27 * This abstract class is designed to work with any AI provider that offers an OpenAI-compatible 28 * API endpoint for image generation, including but not limited to Anthropic, Google, and other 29 * providers that have adopted OpenAI's image generation API specification as a standard interface. 30 * 31 * @since 0.1.0 32 * 33 * @phpstan-type ImageGenerationParams array{ 34 * model: string, 35 * prompt: string, 36 * n?: int, 37 * response_format?: string, 38 * output_format?: string|null, 39 * size?: string, 40 * ... 41 * } 42 * @phpstan-type ChoiceData array{ 43 * url?: string, 44 * b64_json?: string 45 * } 46 * @phpstan-type UsageData array{ 47 * input_tokens?: int, 48 * output_tokens?: int, 49 * total_tokens?: int 50 * } 51 * @phpstan-type ResponseData array{ 52 * id?: string, 53 * data?: list<ChoiceData>, 54 * usage?: UsageData 55 * } 56 */ 57 abstract class AbstractOpenAiCompatibleImageGenerationModel extends AbstractApiBasedModel implements ImageGenerationModelInterface 58 { 59 /** 60 * {@inheritDoc} 61 * 62 * @since 0.1.0 63 */ 64 public function generateImageResult(array $prompt): GenerativeAiResult 65 { 66 $httpTransporter = $this->getHttpTransporter(); 67 $params = $this->prepareGenerateImageParams($prompt); 68 $request = $this->createRequest(HttpMethodEnum::POST(), 'images/generations', ['Content-Type' => 'application/json'], $params); 69 // Add authentication credentials to the request. 70 $request = $this->getRequestAuthentication()->authenticateRequest($request); 71 // Send and process the request. 72 $response = $httpTransporter->send($request); 73 $this->throwIfNotSuccessful($response); 74 return $this->parseResponseToGenerativeAiResult($response, isset($params['output_format']) && is_string($params['output_format']) ? "image/{$params['output_format']}" : 'image/png'); 75 } 76 /** 77 * Prepares the given prompt and the model configuration into parameters for the API request. 78 * 79 * @since 0.1.0 80 * 81 * @param list<Message> $prompt The prompt to generate an image for. Either a single message or a list of messages 82 * from a chat. However as of today, OpenAI compatible image generation endpoints only 83 * support a single user message. 84 * @return ImageGenerationParams The parameters for the API request. 85 */ 86 protected function prepareGenerateImageParams(array $prompt): array 87 { 88 $config = $this->getConfig(); 89 $params = ['model' => $this->metadata()->getId(), 'prompt' => $this->preparePromptParam($prompt)]; 90 $candidateCount = $config->getCandidateCount(); 91 if ($candidateCount !== null) { 92 $params['n'] = $candidateCount; 93 } 94 $outputFileType = $config->getOutputFileType(); 95 if ($outputFileType !== null) { 96 $params['response_format'] = $outputFileType->isRemote() ? 'url' : 'b64_json'; 97 } else { 98 // The 'response_format' parameter is required, so we default to 'b64_json' if not set. 99 $params['response_format'] = 'b64_json'; 100 } 101 $outputMimeType = $config->getOutputMimeType(); 102 if ($outputMimeType !== null) { 103 $params['output_format'] = preg_replace('/^image\//', '', $outputMimeType); 104 } 105 $outputMediaOrientation = $config->getOutputMediaOrientation(); 106 $outputMediaAspectRatio = $config->getOutputMediaAspectRatio(); 107 if ($outputMediaOrientation !== null || $outputMediaAspectRatio !== null) { 108 $params['size'] = $this->prepareSizeParam($outputMediaOrientation, $outputMediaAspectRatio); 109 } 110 /* 111 * Any custom options are added to the parameters as well. 112 * This allows developers to pass other options that may be more niche or not yet supported by the SDK. 113 */ 114 $customOptions = $config->getCustomOptions(); 115 foreach ($customOptions as $key => $value) { 116 if (isset($params[$key])) { 117 throw new InvalidArgumentException(sprintf('The custom option "%s" conflicts with an existing parameter.', $key)); 118 } 119 $params[$key] = $value; 120 } 121 /** @var ImageGenerationParams $params */ 122 return $params; 123 } 124 /** 125 * Prepares the prompt parameter for the API request. 126 * 127 * @since 0.1.0 128 * 129 * @param list<Message> $messages The messages to prepare. However as of today, OpenAI compatible image generation 130 * endpoints only support a single user message. 131 * @return string The prepared prompt parameter. 132 */ 133 protected function preparePromptParam(array $messages): string 134 { 135 if (count($messages) !== 1) { 136 throw new InvalidArgumentException('The API requires a single user message as prompt.'); 137 } 138 $message = $messages[0]; 139 if (!$message->getRole()->isUser()) { 140 throw new InvalidArgumentException('The API requires a user message as prompt.'); 141 } 142 $text = null; 143 foreach ($message->getParts() as $part) { 144 $text = $part->getText(); 145 if ($text !== null) { 146 break; 147 } 148 } 149 if ($text === null) { 150 throw new InvalidArgumentException('The API requires a single text message part as prompt.'); 151 } 152 return $text; 153 } 154 /** 155 * Prepares the size parameter for the API request. 156 * 157 * @since 0.1.0 158 * 159 * @param MediaOrientationEnum|null $orientation The desired media orientation. 160 * @param string|null $aspectRatio The desired media aspect ratio. 161 * @return string The prepared size parameter. 162 */ 163 protected function prepareSizeParam(?MediaOrientationEnum $orientation, ?string $aspectRatio): string 164 { 165 // Use aspect ratio if set, as it is more specific. 166 if ($aspectRatio !== null) { 167 switch ($aspectRatio) { 168 case '1:1': 169 return '1024x1024'; 170 case '3:2': 171 return '1536x1024'; 172 case '7:4': 173 return '1792x1024'; 174 case '2:3': 175 return '1024x1536'; 176 case '4:7': 177 return '1024x1792'; 178 default: 179 throw new InvalidArgumentException('The aspect ratio "' . $aspectRatio . '" is not supported.'); 180 } 181 } 182 // This should always have a value, as the method is only called if at least one or the other is set. 183 if ($orientation !== null) { 184 if ($orientation->isLandscape()) { 185 return '1536x1024'; 186 } 187 if ($orientation->isPortrait()) { 188 return '1024x1536'; 189 } 190 } 191 return '1024x1024'; 192 } 193 /** 194 * Creates a request object for the provider's API. 195 * 196 * Implementations should use $this->getRequestOptions() to attach any 197 * configured request options to the Request. 198 * 199 * @since 0.1.0 200 * 201 * @param HttpMethodEnum $method The HTTP method. 202 * @param string $path The API endpoint path, relative to the base URI. 203 * @param array<string, string|list<string>> $headers The request headers. 204 * @param string|array<string, mixed>|null $data The request data. 205 * @return Request The request object. 206 */ 207 abstract protected function createRequest(HttpMethodEnum $method, string $path, array $headers = [], $data = null): Request; 208 /** 209 * Throws an exception if the response is not successful. 210 * 211 * @since 0.1.0 212 * 213 * @param Response $response The HTTP response to check. 214 * @throws ResponseException If the response is not successful. 215 */ 216 protected function throwIfNotSuccessful(Response $response): void 217 { 218 /* 219 * While this method only calls the utility method, it's important to have it here as a protected method so 220 * that child classes can override it if needed. 221 */ 222 ResponseUtil::throwIfNotSuccessful($response); 223 } 224 /** 225 * Parses the response from the API endpoint to a generative AI result. 226 * 227 * @since 0.1.0 228 * 229 * @param Response $response The response from the API endpoint. 230 * @param string $expectedMimeType The expected MIME type the response is in. 231 * @return GenerativeAiResult The parsed generative AI result. 232 */ 233 protected function parseResponseToGenerativeAiResult(Response $response, string $expectedMimeType = 'image/png'): GenerativeAiResult 234 { 235 /** @var ResponseData $responseData */ 236 $responseData = $response->getData(); 237 if (!isset($responseData['data']) || !$responseData['data']) { 238 throw ResponseException::fromMissingData($this->providerMetadata()->getName(), 'data'); 239 } 240 if (!is_array($responseData['data'])) { 241 throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), 'data', 'The value must be an array.'); 242 } 243 $candidates = []; 244 foreach ($responseData['data'] as $index => $choiceData) { 245 if (!is_array($choiceData) || array_is_list($choiceData)) { 246 throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), "data[{$index}]", 'The value must be an associative array.'); 247 } 248 $candidates[] = $this->parseResponseChoiceToCandidate($choiceData, $index, $expectedMimeType); 249 } 250 $id = $this->getResultId($responseData); 251 if (isset($responseData['usage']) && is_array($responseData['usage'])) { 252 $usage = $responseData['usage']; 253 $tokenUsage = new TokenUsage($usage['input_tokens'] ?? 0, $usage['output_tokens'] ?? 0, $usage['total_tokens'] ?? 0); 254 } else { 255 $tokenUsage = new TokenUsage(0, 0, 0); 256 } 257 // Use any other data from the response as provider-specific response metadata. 258 $providerMetadata = $responseData; 259 unset($providerMetadata['id'], $providerMetadata['data'], $providerMetadata['usage']); 260 return new GenerativeAiResult($id, $candidates, $tokenUsage, $this->providerMetadata(), $this->metadata(), $providerMetadata); 261 } 262 /** 263 * Parses a single choice from the API response into a Candidate object. 264 * 265 * @since 0.1.0 266 * 267 * @param ChoiceData $choiceData The choice data from the API response. 268 * @param int $index The index of the choice in the choices array. 269 * @param string $expectedMimeType The expected MIME type the response is in. 270 * @return Candidate The parsed candidate. 271 * @throws RuntimeException If the choice data is invalid. 272 */ 273 protected function parseResponseChoiceToCandidate(array $choiceData, int $index, string $expectedMimeType = 'image/png'): Candidate 274 { 275 if (isset($choiceData['url']) && is_string($choiceData['url'])) { 276 $imageFile = new File($choiceData['url'], $expectedMimeType); 277 } elseif (isset($choiceData['b64_json']) && is_string($choiceData['b64_json'])) { 278 $imageFile = new File($choiceData['b64_json'], $expectedMimeType); 279 } else { 280 throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), "choices[{$index}]", 'The value must contain either a url or b64_json key with a string value.'); 281 } 282 $parts = [new MessagePart($imageFile)]; 283 $message = new Message(MessageRoleEnum::model(), $parts); 284 return new Candidate($message, FinishReasonEnum::stop()); 285 } 286 /** 287 * Extracts the result ID from the API response data. 288 * 289 * @since 0.4.0 290 * 291 * @param array<string, mixed> $responseData The response data from the API. 292 * @return string The result ID. 293 */ 294 protected function getResultId(array $responseData): string 295 { 296 return isset($responseData['id']) && is_string($responseData['id']) ? $responseData['id'] : ''; 297 } 298 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sat Jun 13 09:38:55 2026 | Cross-referenced by PHPXref |