| [ 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; 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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Wed Oct 14 08:20:33 2026 | Cross-referenced by PHPXref |