[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/php-ai-client/src/Providers/OpenAiCompatibleImplementation/ -> AbstractOpenAiCompatibleImageGenerationModel.php (source)

   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  }


Generated : Sat Jun 13 09:38:55 2026 Cross-referenced by PHPXref