[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

   1  <?php
   2  
   3  declare (strict_types=1);
   4  namespace WordPress\AiClient\Providers;
   5  
   6  use WordPress\AiClient\Common\Exception\InvalidArgumentException;
   7  use WordPress\AiClient\Providers\ApiBasedImplementation\Contracts\ApiBasedModelInterface;
   8  use WordPress\AiClient\Providers\Http\DTO\RequestOptions;
   9  use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
  10  use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
  11  use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
  12  use WordPress\AiClient\Providers\Models\DTO\ModelRequirements;
  13  /**
  14   * Resolves the concrete AI model to use based on selection preferences.
  15   *
  16   * This class owns all model-selection state and logic: an explicitly set model,
  17   * ordered model preferences, a provider constraint, and HTTP request options. It
  18   * is shared between the builders (via the model resolution trait) so that model
  19   * selection behaves identically regardless of what is being generated.
  20   *
  21   * @since 1.4.0
  22   *
  23   * A ProviderModelTuple is a two-element list identifying a model by its provider: index 0 is
  24   * the provider ID or provider class name, index 1 is the model ID. For example,
  25   * ['google', 'gemini-embedding-001'].
  26   *
  27   * @phpstan-type ProviderModelTuple array{0: string, 1: string}
  28   */
  29  class ModelResolver
  30  {
  31      /**
  32       * @var ProviderRegistry The provider registry for finding suitable models.
  33       */
  34      private \WordPress\AiClient\Providers\ProviderRegistry $registry;
  35      /**
  36       * @var ModelInterface|null The model to use for generation.
  37       */
  38      private ?ModelInterface $model = null;
  39      /**
  40       * @var list<string> Ordered list of preference keys to check when selecting a model.
  41       */
  42      private array $modelPreferenceKeys = [];
  43      /**
  44       * @var string|null The provider ID or class name.
  45       */
  46      private ?string $providerIdOrClassName = null;
  47      /**
  48       * @var RequestOptions|null The request options for HTTP transport.
  49       */
  50      private ?RequestOptions $requestOptions = null;
  51      /**
  52       * Constructor.
  53       *
  54       * @since 1.4.0
  55       *
  56       * @param ProviderRegistry $registry The provider registry for finding suitable models.
  57       */
  58      public function __construct(\WordPress\AiClient\Providers\ProviderRegistry $registry)
  59      {
  60          $this->registry = $registry;
  61      }
  62      /**
  63       * Creates a deep clone of this resolver.
  64       *
  65       * Clones the request options (which contain only primitives). The registry and
  66       * explicitly set model are service objects and are intentionally NOT cloned, as
  67       * they should be shared references.
  68       *
  69       * @since 1.4.0
  70       */
  71      public function __clone()
  72      {
  73          if ($this->requestOptions !== null) {
  74              $this->requestOptions = clone $this->requestOptions;
  75          }
  76      }
  77      /**
  78       * Sets the model to use for generation.
  79       *
  80       * @since 1.4.0
  81       *
  82       * @param ModelInterface $model The model to use.
  83       * @return void
  84       */
  85      public function setModel(ModelInterface $model): void
  86      {
  87          $this->model = $model;
  88      }
  89      /**
  90       * Gets the explicitly set model, if any.
  91       *
  92       * @since 1.4.0
  93       *
  94       * @return ModelInterface|null The explicitly set model, or null if none was set.
  95       */
  96      public function getModel(): ?ModelInterface
  97      {
  98          return $this->model;
  99      }
 100      /**
 101       * Sets preferred models to evaluate in order.
 102       *
 103       * @since 1.4.0
 104       *
 105       * @param string|ModelInterface|ProviderModelTuple ...$preferredModels The preferred models as model IDs,
 106       * model instances, or [provider ID, model ID] tuples. For broader compatibility, it is recommended you specify
 107       * only model IDs or model instances, as that will allow for different providers that expose the same model to be
 108       * considered.
 109       * @return void
 110       *
 111       * @throws InvalidArgumentException When a preferred model has an invalid type or identifier.
 112       */
 113      public function setModelPreferences(...$preferredModels): void
 114      {
 115          if ($preferredModels === []) {
 116              throw new InvalidArgumentException('At least one model preference must be provided.');
 117          }
 118          $preferenceKeys = [];
 119          foreach ($preferredModels as $preferredModel) {
 120              if (is_array($preferredModel)) {
 121                  // [model identifier, provider ID] tuple
 122                  if (!array_is_list($preferredModel) || count($preferredModel) !== 2) {
 123                      throw new InvalidArgumentException('Model preference tuple must contain model identifier and provider ID.');
 124                  }
 125                  [$providerId, $modelId] = $preferredModel;
 126                  $modelId = $this->normalizePreferenceIdentifier($modelId);
 127                  $providerId = $this->normalizePreferenceIdentifier($providerId, 'Model preference provider identifiers cannot be empty.');
 128                  $preferenceKey = $this->createProviderModelPreferenceKey($providerId, $modelId);
 129              } elseif ($preferredModel instanceof ModelInterface) {
 130                  // Model instance
 131                  $modelId = $preferredModel->metadata()->getId();
 132                  $providerId = $preferredModel->providerMetadata()->getId();
 133                  $preferenceKey = $this->createProviderModelPreferenceKey($providerId, $modelId);
 134              } elseif (is_string($preferredModel)) {
 135                  // Model ID
 136                  $modelId = $this->normalizePreferenceIdentifier($preferredModel);
 137                  $preferenceKey = $this->createModelPreferenceKey($modelId);
 138              } else {
 139                  // Invalid type
 140                  throw new InvalidArgumentException('Model preferences must be model identifiers, instances of ModelInterface, ' . 'or provider/model tuples.');
 141              }
 142              $preferenceKeys[] = $preferenceKey;
 143          }
 144          $this->modelPreferenceKeys = $preferenceKeys;
 145      }
 146      /**
 147       * Sets the provider to use for generation.
 148       *
 149       * @since 1.4.0
 150       *
 151       * @param string $providerIdOrClassName The provider ID or class name.
 152       * @return void
 153       */
 154      public function setProvider(string $providerIdOrClassName): void
 155      {
 156          $this->providerIdOrClassName = $providerIdOrClassName;
 157      }
 158      /**
 159       * Sets the request options for HTTP transport.
 160       *
 161       * @since 1.4.0
 162       *
 163       * @param RequestOptions $requestOptions The request options.
 164       * @return void
 165       */
 166      public function setRequestOptions(RequestOptions $requestOptions): void
 167      {
 168          $this->requestOptions = $requestOptions;
 169      }
 170      /**
 171       * Resolves the model to use for the given requirements.
 172       *
 173       * If a model has been explicitly set, updates its config, binds dependencies,
 174       * and returns it. Otherwise, finds a suitable model based on the requirements,
 175       * honoring any configured model preferences and provider constraint.
 176       *
 177       * @since 1.4.0
 178       *
 179       * @param ModelRequirements $requirements The requirements the model must satisfy.
 180       * @param ModelConfig $modelConfig The model configuration to apply.
 181       * @param string|null $subject Optional caller-specific subject for failure messages.
 182       * @return ModelInterface The model to use.
 183       * @throws InvalidArgumentException If no suitable model is found or set model doesn't meet requirements.
 184       */
 185      public function resolve(ModelRequirements $requirements, ModelConfig $modelConfig, ?string $subject = null): ModelInterface
 186      {
 187          if ($this->model !== null) {
 188              // Explicit model was provided via setModel(); just update config and bind dependencies.
 189              $model = $this->model;
 190              $model->setConfig($modelConfig);
 191              $this->registry->bindModelDependencies($model);
 192              $this->bindModelRequestOptions($model);
 193              return $model;
 194          }
 195          // Retrieve the candidate models map which satisfies the requirements.
 196          $candidateMap = $this->getCandidateModelsMap($requirements);
 197          if (empty($candidateMap)) {
 198              // The primary capability is always the first required capability (see
 199              // ModelRequirements::fromPromptData()/fromEmbeddingData()).
 200              $requiredCapabilities = $requirements->getRequiredCapabilities();
 201              $primaryCapability = reset($requiredCapabilities);
 202              if ($primaryCapability === \false) {
 203                  $message = 'No models found that meet the requested requirements.';
 204                  if ($this->providerIdOrClassName !== null) {
 205                      $message = sprintf('No models found for provider "%s" that meet the requested requirements.', $this->providerIdOrClassName);
 206                  }
 207                  throw new InvalidArgumentException($message);
 208              }
 209              $capabilityValue = $primaryCapability->value;
 210              $message = sprintf('No models found that support %s.', $capabilityValue);
 211              if ($subject !== null) {
 212                  $message = sprintf('No models found that support %s for this %s.', $capabilityValue, $subject);
 213              }
 214              if ($this->providerIdOrClassName !== null) {
 215                  $message = sprintf('No models found for provider "%s" that support %s.', $this->providerIdOrClassName, $capabilityValue);
 216                  if ($subject !== null) {
 217                      $message = sprintf('No models found for provider "%s" that support %s for this %s.', $this->providerIdOrClassName, $capabilityValue, $subject);
 218                  }
 219              }
 220              throw new InvalidArgumentException($message);
 221          }
 222          // Check if any preferred models match the candidates, in priority order.
 223          if (!empty($this->modelPreferenceKeys)) {
 224              // Find preferences that match available candidates, preserving preference order.
 225              $matchingPreferences = array_intersect_key(array_flip($this->modelPreferenceKeys), $candidateMap);
 226              if (!empty($matchingPreferences)) {
 227                  // Get the first matching preference key
 228                  $firstMatchKey = key($matchingPreferences);
 229                  [$providerId, $modelId] = $candidateMap[$firstMatchKey];
 230                  $model = $this->registry->getProviderModel($providerId, $modelId, $modelConfig);
 231                  $this->bindModelRequestOptions($model);
 232                  return $model;
 233              }
 234          }
 235          // No preference matched; fall back to the first candidate discovered.
 236          [$providerId, $modelId] = reset($candidateMap);
 237          $model = $this->registry->getProviderModel($providerId, $modelId, $modelConfig);
 238          $this->bindModelRequestOptions($model);
 239          return $model;
 240      }
 241      /**
 242       * Checks whether any model can satisfy the given requirements.
 243       *
 244       * @since 1.4.0
 245       *
 246       * @param ModelRequirements $requirements The requirements to check support for.
 247       * @return bool True if the set model or any registered model meets the requirements.
 248       */
 249      public function isSupported(ModelRequirements $requirements): bool
 250      {
 251          // If the model has been set, check if it meets the requirements
 252          if ($this->model !== null) {
 253              return $requirements->areMetBy($this->model->metadata());
 254          }
 255          try {
 256              // Check if any models support these requirements
 257              $models = $this->registry->findModelsMetadataForSupport($requirements);
 258              return !empty($models);
 259          } catch (InvalidArgumentException $e) {
 260              // No models support the requirements
 261              return \false;
 262          }
 263      }
 264      /**
 265       * Binds configured request options to the model if present and supported.
 266       *
 267       * Request options are only applicable to API-based models that make HTTP requests.
 268       *
 269       * @since 1.4.0
 270       *
 271       * @param ModelInterface $model The model to bind request options to.
 272       * @return void
 273       */
 274      private function bindModelRequestOptions(ModelInterface $model): void
 275      {
 276          if ($this->requestOptions !== null && $model instanceof ApiBasedModelInterface) {
 277              $model->setRequestOptions($this->requestOptions);
 278          }
 279      }
 280      /**
 281       * Builds a map of candidate models that satisfy the requirements for efficient lookup.
 282       *
 283       * @since 1.4.0
 284       *
 285       * @param ModelRequirements $requirements The requirements derived from the prompt.
 286       * @return array<string, ProviderModelTuple> Map of preference keys to [providerId, modelId] tuples.
 287       */
 288      private function getCandidateModelsMap(ModelRequirements $requirements): array
 289      {
 290          if ($this->providerIdOrClassName === null) {
 291              // No provider locked in, gather all models across providers that meet requirements.
 292              $providerModelsMetadata = $this->registry->findModelsMetadataForSupport($requirements);
 293              $candidateMap = [];
 294              foreach ($providerModelsMetadata as $providerModels) {
 295                  $providerId = $providerModels->getProvider()->getId();
 296                  $providerMap = $this->generateMapFromCandidates($providerId, $providerModels->getModels());
 297                  // Use + operator to merge, preserving keys from $candidateMap (first provider wins for model-only keys)
 298                  $candidateMap = $candidateMap + $providerMap;
 299              }
 300              return $candidateMap;
 301          }
 302          // Provider set, only consider models from that provider.
 303          $modelsMetadata = $this->registry->findProviderModelsMetadataForSupport($this->providerIdOrClassName, $requirements);
 304          // Ensure we pass the provider ID, not the class name
 305          $providerId = $this->registry->getProviderId($this->providerIdOrClassName);
 306          return $this->generateMapFromCandidates($providerId, $modelsMetadata);
 307      }
 308      /**
 309       * Generates a candidate map from model metadata with both provider-specific and model-only keys.
 310       *
 311       * @since 1.4.0
 312       *
 313       * @param string $providerId The provider ID.
 314       * @param list<ModelMetadata> $modelsMetadata The models metadata to map.
 315       * @return array<string, ProviderModelTuple> Map of preference keys to [providerId, modelId] tuples.
 316       */
 317      private function generateMapFromCandidates(string $providerId, array $modelsMetadata): array
 318      {
 319          $map = [];
 320          foreach ($modelsMetadata as $modelMetadata) {
 321              $modelId = $modelMetadata->getId();
 322              // Add provider-specific key
 323              $providerModelKey = $this->createProviderModelPreferenceKey($providerId, $modelId);
 324              $map[$providerModelKey] = [$providerId, $modelId];
 325              // Add model-only key
 326              $modelKey = $this->createModelPreferenceKey($modelId);
 327              $map[$modelKey] = [$providerId, $modelId];
 328          }
 329          return $map;
 330      }
 331      /**
 332       * Normalizes and validates a preference identifier string.
 333       *
 334       * @since 1.4.0
 335       *
 336       * @param mixed $value The value to normalize.
 337       * @param string $emptyMessage The message for empty or invalid values.
 338       * @return string The normalized identifier.
 339       *
 340       * @throws InvalidArgumentException If the value is not a non-empty string.
 341       */
 342      private function normalizePreferenceIdentifier($value, string $emptyMessage = 'Model preference identifiers cannot be empty.'): string
 343      {
 344          if (!is_string($value)) {
 345              throw new InvalidArgumentException($emptyMessage);
 346          }
 347          $trimmed = trim($value);
 348          if ($trimmed === '') {
 349              throw new InvalidArgumentException($emptyMessage);
 350          }
 351          return $trimmed;
 352      }
 353      /**
 354       * Creates a preference key for a provider/model combination.
 355       *
 356       * @since 1.4.0
 357       *
 358       * @param string $providerId The provider identifier.
 359       * @param string $modelId The model identifier.
 360       * @return string The generated preference key.
 361       */
 362      private function createProviderModelPreferenceKey(string $providerId, string $modelId): string
 363      {
 364          return 'providerModel::' . $providerId . '::' . $modelId;
 365      }
 366      /**
 367       * Creates a preference key for a model identifier.
 368       *
 369       * @since 1.4.0
 370       *
 371       * @param string $modelId The model identifier.
 372       * @return string The generated preference key.
 373       */
 374      private function createModelPreferenceKey(string $modelId): string
 375      {
 376          return 'model::' . $modelId;
 377      }
 378  }


Generated : Wed Oct 14 08:20:33 2026 Cross-referenced by PHPXref