[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

   1  <?php
   2  
   3  declare (strict_types=1);
   4  namespace WordPress\AiClient\Providers;
   5  
   6  use WordPress\AiClientDependencies\Http\Discovery\Exception\NotFoundException as DiscoveryNotFoundException;
   7  use WordPress\AiClient\Common\Exception\InvalidArgumentException;
   8  use WordPress\AiClient\Common\Exception\RuntimeException;
   9  use WordPress\AiClient\Providers\Contracts\ProviderInterface;
  10  use WordPress\AiClient\Providers\Contracts\ProviderWithOperationsHandlerInterface;
  11  use WordPress\AiClient\Providers\DTO\ProviderMetadata;
  12  use WordPress\AiClient\Providers\DTO\ProviderModelsMetadata;
  13  use WordPress\AiClient\Providers\Http\Contracts\HttpTransporterInterface;
  14  use WordPress\AiClient\Providers\Http\Contracts\RequestAuthenticationInterface;
  15  use WordPress\AiClient\Providers\Http\Contracts\WithHttpTransporterInterface;
  16  use WordPress\AiClient\Providers\Http\Contracts\WithRequestAuthenticationInterface;
  17  use WordPress\AiClient\Providers\Http\HttpTransporterFactory;
  18  use WordPress\AiClient\Providers\Http\Traits\WithHttpTransporterTrait;
  19  use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
  20  use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
  21  use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
  22  use WordPress\AiClient\Providers\Models\DTO\ModelRequirements;
  23  /**
  24   * Registry for managing AI providers and their models.
  25   *
  26   * This class provides a centralized way to register AI providers, discover
  27   * their capabilities, and find suitable models based on requirements.
  28   *
  29   * @since 0.1.0
  30   */
  31  class ProviderRegistry implements WithHttpTransporterInterface
  32  {
  33      use WithHttpTransporterTrait {
  34          setHttpTransporter as setHttpTransporterOriginal;
  35      }
  36      /**
  37       * @var array<string, class-string<ProviderInterface>> Mapping of provider IDs to class names.
  38       */
  39      private array $registeredIdsToClassNames = [];
  40      /**
  41       * @var array<class-string<ProviderInterface>, string> Mapping of provider class names to IDs.
  42       */
  43      private array $registeredClassNamesToIds = [];
  44      /**
  45       * @var array<class-string<ProviderInterface>, RequestAuthenticationInterface> Mapping of provider class names to
  46       *                                                                             authentication instances.
  47       */
  48      private array $providerAuthenticationInstances = [];
  49      /**
  50       * Registers a provider class with the registry.
  51       *
  52       * @since 0.1.0
  53       *
  54       * @param class-string<ProviderInterface> $className The fully qualified provider class name implementing the
  55       * ProviderInterface
  56       * @throws InvalidArgumentException If the class doesn't exist or implement the required interface.
  57       */
  58      public function registerProvider(string $className): void
  59      {
  60          if (!class_exists($className)) {
  61              throw new InvalidArgumentException(sprintf('Provider class does not exist: %s', $className));
  62          }
  63          // Validate that class implements ProviderInterface
  64          if (!is_subclass_of($className, ProviderInterface::class)) {
  65              throw new InvalidArgumentException(sprintf('Provider class must implement %s: %s', ProviderInterface::class, $className));
  66          }
  67          $metadata = $className::metadata();
  68          if (!$metadata instanceof ProviderMetadata) {
  69              throw new InvalidArgumentException(sprintf('Provider must return ProviderMetadata from metadata() method: %s', $className));
  70          }
  71          // If there is already a HTTP transporter instance set, hook it up to the provider as needed.
  72          try {
  73              $httpTransporter = $this->getHttpTransporter();
  74          } catch (RuntimeException $e) {
  75              /*
  76               * If this fails, it's okay. There is no defined sequence between setting the HTTP transporter in the
  77               * registry and registering providers in it, so it might be that the transporter is set later. It will be
  78               * hooked up then.
  79               * But for now we can ignore this exception and attempt to set the default HTTP transporter, if possible.
  80               */
  81              try {
  82                  $this->setHttpTransporter(HttpTransporterFactory::createTransporter());
  83                  $httpTransporter = $this->getHttpTransporter();
  84              } catch (DiscoveryNotFoundException $e) {
  85                  /*
  86                   * If no HTTP client implementation can be discovered yet, we can ignore this for now.
  87                   * It might be set later, so it's not a hard error at this point.
  88                   * We'll try again the next time a provider is registered, or maybe by that time an explicit
  89                   * HTTP transporter will have been set.
  90                   */
  91              }
  92          }
  93          if (isset($httpTransporter)) {
  94              $this->setHttpTransporterForProvider($className, $httpTransporter);
  95          }
  96          // Hook up the request authentication instance, using a default if not set.
  97          if (!isset($this->providerAuthenticationInstances[$className])) {
  98              $defaultProviderAuthentication = $this->createDefaultProviderRequestAuthentication($className);
  99              if ($defaultProviderAuthentication !== null) {
 100                  $this->providerAuthenticationInstances[$className] = $defaultProviderAuthentication;
 101              }
 102          }
 103          if (isset($this->providerAuthenticationInstances[$className])) {
 104              $this->setRequestAuthenticationForProvider($className, $this->providerAuthenticationInstances[$className]);
 105          }
 106          $this->registeredIdsToClassNames[$metadata->getId()] = $className;
 107          $this->registeredClassNamesToIds[$className] = $metadata->getId();
 108      }
 109      /**
 110       * Gets a list of all registered provider IDs.
 111       *
 112       * @since 0.1.0
 113       *
 114       * @return list<string> List of registered provider IDs.
 115       */
 116      public function getRegisteredProviderIds(): array
 117      {
 118          return array_keys($this->registeredIdsToClassNames);
 119      }
 120      /**
 121       * Checks if a provider is registered.
 122       *
 123       * @since 0.1.0
 124       *
 125       * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name to check.
 126       * @return bool True if the provider is registered.
 127       */
 128      public function hasProvider(string $idOrClassName): bool
 129      {
 130          return $this->isRegisteredId($idOrClassName) || $this->isRegisteredClassName($idOrClassName);
 131      }
 132      /**
 133       * Gets the class name for a registered provider.
 134       *
 135       * @since 0.1.0
 136       *
 137       * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
 138       * @return class-string<ProviderInterface> The provider class name.
 139       * @throws InvalidArgumentException If the provider is not registered.
 140       */
 141      public function getProviderClassName(string $idOrClassName): string
 142      {
 143          // If it's already a class name, return it
 144          if ($this->isRegisteredClassName($idOrClassName)) {
 145              return $idOrClassName;
 146          }
 147          // If it's a registered ID, return its class name
 148          if ($this->isRegisteredId($idOrClassName)) {
 149              return $this->registeredIdsToClassNames[$idOrClassName];
 150          }
 151          // Not found
 152          throw new InvalidArgumentException(sprintf('Provider not registered: %s', $idOrClassName));
 153      }
 154      /**
 155       * Gets the provider ID for a registered provider.
 156       *
 157       * @since 0.2.0
 158       *
 159       * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
 160       * @return string The provider ID.
 161       * @throws InvalidArgumentException If the provider is not registered.
 162       */
 163      public function getProviderId(string $idOrClassName): string
 164      {
 165          // If it's already an ID, return it
 166          if ($this->isRegisteredId($idOrClassName)) {
 167              return $idOrClassName;
 168          }
 169          // If it's a registered class name, return its ID
 170          if ($this->isRegisteredClassName($idOrClassName)) {
 171              return $this->registeredClassNamesToIds[$idOrClassName];
 172          }
 173          // Not found
 174          throw new InvalidArgumentException(sprintf('Provider not registered: %s', $idOrClassName));
 175      }
 176      /**
 177       * Checks if a provider is properly configured.
 178       *
 179       * @since 0.1.0
 180       *
 181       * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
 182       * @return bool True if the provider is configured and ready to use.
 183       */
 184      public function isProviderConfigured(string $idOrClassName): bool
 185      {
 186          try {
 187              $className = $this->resolveProviderClassName($idOrClassName);
 188              // Use static method from ProviderInterface
 189              /** @var class-string<ProviderInterface> $className */
 190              $availability = $className::availability();
 191              return $availability->isConfigured();
 192          } catch (InvalidArgumentException $e) {
 193              return \false;
 194          }
 195      }
 196      /**
 197       * Finds models across all available providers that support the given requirements.
 198       *
 199       * @since 0.1.0
 200       *
 201       * @param ModelRequirements $modelRequirements The requirements to match against.
 202       * @return list<ProviderModelsMetadata> List of provider models metadata that match requirements.
 203       */
 204      public function findModelsMetadataForSupport(ModelRequirements $modelRequirements): array
 205      {
 206          $results = [];
 207          foreach ($this->registeredIdsToClassNames as $providerId => $className) {
 208              $providerResults = $this->findProviderModelsMetadataForSupport($providerId, $modelRequirements);
 209              if (!empty($providerResults)) {
 210                  // Use static method from ProviderInterface
 211                  /** @var class-string<ProviderInterface> $className */
 212                  $providerMetadata = $className::metadata();
 213                  $results[] = new ProviderModelsMetadata($providerMetadata, $providerResults);
 214              }
 215          }
 216          return $results;
 217      }
 218      /**
 219       * Finds models within a specific available provider that support the given requirements.
 220       *
 221       * @since 0.1.0
 222       *
 223       * @param string $idOrClassName The provider ID or class name.
 224       * @param ModelRequirements $modelRequirements The requirements to match against.
 225       * @return list<ModelMetadata> List of model metadata that match requirements.
 226       */
 227      public function findProviderModelsMetadataForSupport(string $idOrClassName, ModelRequirements $modelRequirements): array
 228      {
 229          $className = $this->resolveProviderClassName($idOrClassName);
 230          // If the provider is not configured, there is no way to use it, so it is considered unavailable.
 231          if (!$this->isProviderConfigured($className)) {
 232              return [];
 233          }
 234          $modelMetadataDirectory = $className::modelMetadataDirectory();
 235          // Filter models that meet requirements
 236          $matchingModels = [];
 237          foreach ($modelMetadataDirectory->listModelMetadata() as $modelMetadata) {
 238              if ($modelRequirements->areMetBy($modelMetadata)) {
 239                  $matchingModels[] = $modelMetadata;
 240              }
 241          }
 242          return $matchingModels;
 243      }
 244      /**
 245       * Gets a configured model instance from a provider.
 246       *
 247       * @since 0.1.0
 248       *
 249       * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
 250       * @param string $modelId The model identifier.
 251       * @param ModelConfig|null $modelConfig The model configuration.
 252       * @return ModelInterface The configured model instance.
 253       * @throws InvalidArgumentException If provider or model is not found.
 254       */
 255      public function getProviderModel(string $idOrClassName, string $modelId, ?ModelConfig $modelConfig = null): ModelInterface
 256      {
 257          $className = $this->resolveProviderClassName($idOrClassName);
 258          $modelInstance = $className::model($modelId, $modelConfig);
 259          $this->bindModelDependencies($modelInstance);
 260          return $modelInstance;
 261      }
 262      /**
 263       * Binds dependencies to a model instance.
 264       *
 265       * This method injects required dependencies such as HTTP transporter
 266       * and authentication into model instances that need them.
 267       *
 268       * @since 0.1.0
 269       *
 270       * @param ModelInterface $modelInstance The model instance to bind dependencies to.
 271       * @return void
 272       */
 273      public function bindModelDependencies(ModelInterface $modelInstance): void
 274      {
 275          $className = $this->resolveProviderClassName($modelInstance->providerMetadata()->getId());
 276          if ($modelInstance instanceof WithHttpTransporterInterface) {
 277              $modelInstance->setHttpTransporter($this->getHttpTransporter());
 278          }
 279          if ($modelInstance instanceof WithRequestAuthenticationInterface) {
 280              $requestAuthentication = $this->getProviderRequestAuthentication($className);
 281              if ($requestAuthentication !== null) {
 282                  $modelInstance->setRequestAuthentication($requestAuthentication);
 283              }
 284          }
 285      }
 286      /**
 287       * Gets the class name for a registered provider (handles both ID and class name input).
 288       *
 289       * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
 290       * @return class-string<ProviderInterface> The provider class name.
 291       * @throws InvalidArgumentException If provider is not registered.
 292       */
 293      private function resolveProviderClassName(string $idOrClassName): string
 294      {
 295          // If it's already a class name, return it
 296          if ($this->isRegisteredClassName($idOrClassName)) {
 297              return $idOrClassName;
 298          }
 299          // If it's a registered ID, return its class name
 300          if ($this->isRegisteredId($idOrClassName)) {
 301              return $this->registeredIdsToClassNames[$idOrClassName];
 302          }
 303          // Not found
 304          throw new InvalidArgumentException(sprintf('Provider not registered: %s', $idOrClassName));
 305      }
 306      /**
 307       * {@inheritDoc}
 308       *
 309       * @since 0.1.0
 310       */
 311      public function setHttpTransporter(HttpTransporterInterface $httpTransporter): void
 312      {
 313          $this->setHttpTransporterOriginal($httpTransporter);
 314          // Make sure all registered providers have the HTTP transporter hooked up as needed.
 315          foreach ($this->registeredIdsToClassNames as $className) {
 316              $this->setHttpTransporterForProvider($className, $httpTransporter);
 317          }
 318      }
 319      /**
 320       * Sets the request authentication instance for the given provider.
 321       *
 322       * @since 0.1.0
 323       *
 324       * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
 325       * @param RequestAuthenticationInterface $requestAuthentication The request authentication instance.
 326       */
 327      public function setProviderRequestAuthentication(string $idOrClassName, RequestAuthenticationInterface $requestAuthentication): void
 328      {
 329          $className = $this->resolveProviderClassName($idOrClassName);
 330          $this->providerAuthenticationInstances[$className] = $requestAuthentication;
 331          $this->setRequestAuthenticationForProvider($className, $requestAuthentication);
 332      }
 333      /**
 334       * Gets the request authentication instance for the given provider, if set.
 335       *
 336       * @since 0.1.0
 337       *
 338       * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
 339       * @return ?RequestAuthenticationInterface The request authentication instance, or null if not set.
 340       */
 341      public function getProviderRequestAuthentication(string $idOrClassName): ?RequestAuthenticationInterface
 342      {
 343          $className = $this->resolveProviderClassName($idOrClassName);
 344          if (!isset($this->providerAuthenticationInstances[$className])) {
 345              return null;
 346          }
 347          return $this->providerAuthenticationInstances[$className];
 348      }
 349      /**
 350       * Sets the HTTP transporter for a specific provider, hooking up its class instances.
 351       *
 352       * @since 0.1.0
 353       *
 354       * @param class-string<ProviderInterface> $className The provider class name.
 355       * @param HttpTransporterInterface $httpTransporter The HTTP transporter instance.
 356       */
 357      private function setHttpTransporterForProvider(string $className, HttpTransporterInterface $httpTransporter): void
 358      {
 359          $availability = $className::availability();
 360          if ($availability instanceof WithHttpTransporterInterface) {
 361              $availability->setHttpTransporter($httpTransporter);
 362          }
 363          $modelMetadataDirectory = $className::modelMetadataDirectory();
 364          if ($modelMetadataDirectory instanceof WithHttpTransporterInterface) {
 365              $modelMetadataDirectory->setHttpTransporter($httpTransporter);
 366          }
 367          if (is_subclass_of($className, ProviderWithOperationsHandlerInterface::class)) {
 368              $operationsHandler = $className::operationsHandler();
 369              if ($operationsHandler instanceof WithHttpTransporterInterface) {
 370                  $operationsHandler->setHttpTransporter($httpTransporter);
 371              }
 372          }
 373      }
 374      /**
 375       * Sets the request authentication for a specific provider, hooking up its class instances.
 376       *
 377       * @since 0.1.0
 378       *
 379       * @param class-string<ProviderInterface> $className The provider class name.
 380       * @param RequestAuthenticationInterface $requestAuthentication The authentication instance.
 381       *
 382       * @throws InvalidArgumentException If the authentication instance is not of the expected type.
 383       */
 384      private function setRequestAuthenticationForProvider(string $className, RequestAuthenticationInterface $requestAuthentication): void
 385      {
 386          $authenticationMethod = $className::metadata()->getAuthenticationMethod();
 387          if ($authenticationMethod === null) {
 388              throw new InvalidArgumentException(sprintf('Provider %s does not expect any authentication, but got %s.', $className, get_class($requestAuthentication)));
 389          }
 390          $expectedClass = $authenticationMethod->getImplementationClass();
 391          if (!$requestAuthentication instanceof $expectedClass) {
 392              throw new InvalidArgumentException(sprintf('Provider %s expects authentication of type %s, but got %s.', $className, $expectedClass, get_class($requestAuthentication)));
 393          }
 394          $availability = $className::availability();
 395          if ($availability instanceof WithRequestAuthenticationInterface) {
 396              $availability->setRequestAuthentication($requestAuthentication);
 397          }
 398          $modelMetadataDirectory = $className::modelMetadataDirectory();
 399          if ($modelMetadataDirectory instanceof WithRequestAuthenticationInterface) {
 400              $modelMetadataDirectory->setRequestAuthentication($requestAuthentication);
 401          }
 402          if (is_subclass_of($className, ProviderWithOperationsHandlerInterface::class)) {
 403              $operationsHandler = $className::operationsHandler();
 404              if ($operationsHandler instanceof WithRequestAuthenticationInterface) {
 405                  $operationsHandler->setRequestAuthentication($requestAuthentication);
 406              }
 407          }
 408      }
 409      /**
 410       * Creates a default request authentication instance for a provider.
 411       *
 412       * @since 0.1.0
 413       *
 414       * @param class-string<ProviderInterface> $className The provider class name.
 415       * @return ?RequestAuthenticationInterface The default request authentication instance, or null if not required or
 416       *                                         if no credential data can be found.
 417       */
 418      private function createDefaultProviderRequestAuthentication(string $className): ?RequestAuthenticationInterface
 419      {
 420          $providerMetadata = $className::metadata();
 421          $providerId = $providerMetadata->getId();
 422          $authenticationMethod = $providerMetadata->getAuthenticationMethod();
 423          if ($authenticationMethod === null) {
 424              return null;
 425          }
 426          $authenticationClass = $authenticationMethod->getImplementationClass();
 427          if ($authenticationClass === null) {
 428              return null;
 429          }
 430          $authenticationSchema = $authenticationClass::getJsonSchema();
 431          // Iterate over all JSON schema object properties to try to determine the necessary authentication data.
 432          $authenticationData = [];
 433          if (isset($authenticationSchema['properties']) && is_array($authenticationSchema['properties'])) {
 434              /** @var array<string, mixed> $details */
 435              foreach ($authenticationSchema['properties'] as $property => $details) {
 436                  $envVarName = $this->getEnvVarName($providerId, $property);
 437                  // Try to get the value from environment variable or constant.
 438                  $envValue = getenv($envVarName);
 439                  if ($envValue === \false) {
 440                      if (!defined($envVarName)) {
 441                          continue;
 442                          // Skip if neither environment variable nor constant is defined.
 443                      }
 444                      $envValue = constant($envVarName);
 445                      if (!is_scalar($envValue)) {
 446                          continue;
 447                      }
 448                  }
 449                  if (isset($details['type'])) {
 450                      switch ($details['type']) {
 451                          case 'boolean':
 452                              $authenticationData[$property] = filter_var($envValue, \FILTER_VALIDATE_BOOLEAN);
 453                              break;
 454                          case 'number':
 455                              $authenticationData[$property] = (int) $envValue;
 456                              break;
 457                          case 'string':
 458                          default:
 459                              $authenticationData[$property] = (string) $envValue;
 460                      }
 461                  } else {
 462                      // Default to string if no type is specified.
 463                      $authenticationData[$property] = (string) $envValue;
 464                  }
 465              }
 466              // If any required fields are missing, return null to avoid immediate errors.
 467              if (isset($authenticationSchema['required']) && is_array($authenticationSchema['required'])) {
 468                  /** @var list<string> $requiredProperties */
 469                  $requiredProperties = $authenticationSchema['required'];
 470                  if (array_diff_key(array_flip($requiredProperties), $authenticationData)) {
 471                      return null;
 472                  }
 473              }
 474          }
 475          /** @var RequestAuthenticationInterface */
 476          /** @var array<string, mixed> $authenticationData */
 477          return $authenticationClass::fromArray($authenticationData);
 478      }
 479      /**
 480       * Checks if the given value is a registered provider class name.
 481       *
 482       * @since 0.4.0
 483       *
 484       * @param string $idOrClassName The value to check.
 485       * @return bool True if it's a registered class name.
 486       * @phpstan-assert-if-true class-string<ProviderInterface> $idOrClassName
 487       */
 488      private function isRegisteredClassName(string $idOrClassName): bool
 489      {
 490          return isset($this->registeredClassNamesToIds[$idOrClassName]);
 491      }
 492      /**
 493       * Checks if the given value is a registered provider ID.
 494       *
 495       * @since 0.4.0
 496       *
 497       * @param string $idOrClassName The value to check.
 498       * @return bool True if it's a registered provider ID.
 499       */
 500      private function isRegisteredId(string $idOrClassName): bool
 501      {
 502          return isset($this->registeredIdsToClassNames[$idOrClassName]);
 503      }
 504      /**
 505       * Converts a provider ID and field name to a constant case environment variable name.
 506       *
 507       * @since 0.1.0
 508       *
 509       * @param string $providerId The provider ID.
 510       * @param string $field The field name.
 511       * @return string The environment variable name in CONSTANT_CASE.
 512       */
 513      private function getEnvVarName(string $providerId, string $field): string
 514      {
 515          // Convert camelCase or kebab-case or snake_case to CONSTANT_CASE.
 516          $constantCaseProviderId = strtoupper((string) preg_replace('/([a-z])([A-Z])/', '$1_$2', str_replace('-', '_', $providerId)));
 517          $constantCaseField = strtoupper((string) preg_replace('/([a-z])([A-Z])/', '$1_$2', str_replace('-', '_', $field)));
 518          return "{$constantCaseProviderId}_{$constantCaseField}";
 519      }
 520  }


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