-
Notifications
You must be signed in to change notification settings - Fork 34
Implement ProviderRegistry with comprehensive test suite #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
JasonTheAdams
merged 40 commits into
WordPress:trunk
from
Ref34t:feature/provider-registry
Aug 15, 2025
Merged
Changes from 12 commits
Commits
Show all changes
40 commits
Select commit
Hold shift + click to select a range
3c56f68
Implement AiProviderRegistry with comprehensive test suite
4b09a07
Fix PHPStan type issues in AiProviderRegistry
a84a4ac
Remove trailing whitespace from AiProviderRegistry
40b926b
Integrate AiProviderRegistry with Provider interfaces from PR #35
7bdcb9c
Fix PHPCS style violations
877fc55
Fix PHPCS style after rebase
cacb2dc
Fix tokenCount references in test files after rebase
63fe203
Fix PHPCS line length warnings in test files
b1e63d5
Fix duplicate meetsRequirements() method after rebase
37d1124
Resolve merge conflict in ModelConfigTest
83927be
Clean up PR #38: Revert unrelated changes per Felix's feedback
d77b021
Fix MockModel method signature to match trunk ModelInterface
06411b9
refactor: renamed to ProviderRegistry
JasonTheAdams b44f26c
fix: removes unused imports
JasonTheAdams cbf1aef
Merge remote-tracking branch 'origin/trunk' into feature/provider-reg…
7956769
Merge remote-tracking branch 'fork/feature/provider-registry' into fe…
4ac777e
fix: not needed because it's used in the parameter
57dfe25
refractor: type fixing
8f96b26
refractor : type fix
766fcf8
refractor : type fix
8a64e8a
refractor : type fix
23a2a01
refractor : clean not needed
14e650b
refractor : type fix
2aeac0e
fix : Validate that class implements ProviderInterface
212bab6
refractor: Add proper PHPStan annotations for static method calls on …
4258145
refractor: type fix
da98ac1
refractor: type fix
8de52d2
refractor: type fix
2f1d300
refactor: reorganize test structure and rename ProviderRegistry test
db59e19
fix: line length to match phpcs
2c9ff36
Fix import sorting in ProviderRegistryTest
03310ba
fix: type
489a4ed
refactor: remove redundant PHPStan annotation
cafc630
Fix PHPCS style violations
df20ba4
Fix PHPCS line length warnings in test files
c52f1d9
test: fixes broken tests from rebase
JasonTheAdams 3abe579
test: reverts tests back to trunk affected by rebase
JasonTheAdams 45de1fa
test: adds missing EOL
JasonTheAdams 6e25240
Optimize ProviderRegistry::hasProvider() performance by replacing in_…
f868e7b
Remove redundant interface validation in resolveProviderClassName
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,229 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace WordPress\AiClient\Providers; | ||
|
|
||
| use InvalidArgumentException; | ||
| use WordPress\AiClient\Providers\Contracts\ModelMetadataDirectoryInterface; | ||
| use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface; | ||
| use WordPress\AiClient\Providers\Contracts\ProviderInterface; | ||
| use WordPress\AiClient\Providers\DTO\ProviderMetadata; | ||
| use WordPress\AiClient\Providers\DTO\ProviderModelsMetadata; | ||
| use WordPress\AiClient\Providers\Models\Contracts\ModelInterface; | ||
| use WordPress\AiClient\Providers\Models\DTO\ModelConfig; | ||
| use WordPress\AiClient\Providers\Models\DTO\ModelMetadata; | ||
| use WordPress\AiClient\Providers\Models\DTO\ModelRequirements; | ||
|
|
||
| /** | ||
| * Registry for managing AI providers and their models. | ||
| * | ||
| * This class provides a centralized way to register AI providers, discover | ||
| * their capabilities, and find suitable models based on requirements. | ||
| * | ||
| * @since n.e.x.t | ||
| */ | ||
| class AiProviderRegistry | ||
| { | ||
| /** | ||
| * @var array<string, string> Mapping of provider IDs to class names. | ||
Ref34t marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| */ | ||
| private array $providerClassNames = []; | ||
|
|
||
|
|
||
| /** | ||
| * Registers a provider class with the registry. | ||
| * | ||
| * @since n.e.x.t | ||
| * | ||
| * @param string $className The fully qualified provider class name. | ||
Ref34t marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * @throws InvalidArgumentException If the class doesn't exist or implement required interface. | ||
| */ | ||
| public function registerProvider(string $className): void | ||
| { | ||
| if (!class_exists($className)) { | ||
| throw new InvalidArgumentException( | ||
| sprintf('Provider class does not exist: %s', $className) | ||
| ); | ||
| } | ||
|
|
||
| // Validate that class implements ProviderInterface | ||
| if (!is_subclass_of($className, ProviderInterface::class)) { | ||
| throw new InvalidArgumentException( | ||
| sprintf('Provider class must implement %s: %s', ProviderInterface::class, $className) | ||
| ); | ||
| } | ||
|
|
||
| // Get provider metadata to extract ID (using static method from interface) | ||
| /** @var class-string<ProviderInterface> $className */ | ||
Ref34t marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| $metadata = $className::metadata(); | ||
|
|
||
| if (!$metadata instanceof ProviderMetadata) { | ||
| throw new InvalidArgumentException( | ||
| sprintf('Provider must return ProviderMetadata from metadata() method: %s', $className) | ||
| ); | ||
| } | ||
|
|
||
| $this->providerClassNames[$metadata->getId()] = $className; | ||
| } | ||
|
|
||
| /** | ||
| * Checks if a provider is registered. | ||
| * | ||
| * @since n.e.x.t | ||
| * | ||
| * @param string $idOrClassName The provider ID or class name to check. | ||
Ref34t marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * @return bool True if the provider is registered. | ||
| */ | ||
| public function hasProvider(string $idOrClassName): bool | ||
| { | ||
| return isset($this->providerClassNames[$idOrClassName]) || | ||
| in_array($idOrClassName, $this->providerClassNames, true); | ||
| } | ||
|
|
||
| /** | ||
| * Gets the class name for a registered provider. | ||
| * | ||
| * @since n.e.x.t | ||
| * | ||
| * @param string $id The provider ID. | ||
| * @return string The provider class name. | ||
| * @throws InvalidArgumentException If the provider is not registered. | ||
| */ | ||
| public function getProviderClassName(string $id): string | ||
| { | ||
| if (!isset($this->providerClassNames[$id])) { | ||
| throw new InvalidArgumentException( | ||
| sprintf('Provider not registered: %s', $id) | ||
| ); | ||
| } | ||
|
|
||
| return $this->providerClassNames[$id]; | ||
| } | ||
|
|
||
| /** | ||
| * Checks if a provider is properly configured. | ||
| * | ||
| * @since n.e.x.t | ||
| * | ||
| * @param string $idOrClassName The provider ID or class name. | ||
Ref34t marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * @return bool True if the provider is configured and ready to use. | ||
| */ | ||
| public function isProviderConfigured(string $idOrClassName): bool | ||
| { | ||
| try { | ||
| $className = $this->resolveProviderClassName($idOrClassName); | ||
|
|
||
| // Use static method from ProviderInterface | ||
| /** @var class-string<ProviderInterface> $className */ | ||
| $availability = $className::availability(); | ||
|
|
||
| return $availability->isConfigured(); | ||
| } catch (InvalidArgumentException $e) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Finds models across all providers that support the given requirements. | ||
| * | ||
| * @since n.e.x.t | ||
| * | ||
| * @param ModelRequirements $modelRequirements The requirements to match against. | ||
| * @return list<ProviderModelsMetadata> List of provider models metadata that match requirements. | ||
| */ | ||
| public function findModelsMetadataForSupport(ModelRequirements $modelRequirements): array | ||
| { | ||
| $results = []; | ||
|
|
||
| foreach ($this->providerClassNames as $providerId => $className) { | ||
| $providerResults = $this->findProviderModelsMetadataForSupport($providerId, $modelRequirements); | ||
| if (!empty($providerResults)) { | ||
| // Use static method from ProviderInterface | ||
| /** @var class-string<ProviderInterface> $className */ | ||
| $providerMetadata = $className::metadata(); | ||
|
|
||
| $results[] = new ProviderModelsMetadata( | ||
| $providerMetadata, | ||
| $providerResults | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| return $results; | ||
| } | ||
|
|
||
| /** | ||
| * Finds models within a specific provider that support the given requirements. | ||
| * | ||
| * @since n.e.x.t | ||
| * | ||
| * @param string $idOrClassName The provider ID or class name. | ||
| * @param ModelRequirements $modelRequirements The requirements to match against. | ||
| * @return list<ModelMetadata> List of model metadata that match requirements. | ||
| */ | ||
| public function findProviderModelsMetadataForSupport( | ||
| string $idOrClassName, | ||
| ModelRequirements $modelRequirements | ||
| ): array { | ||
| $className = $this->resolveProviderClassName($idOrClassName); | ||
|
|
||
| // Use static method from ProviderInterface | ||
| /** @var class-string<ProviderInterface> $className */ | ||
Ref34t marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| $modelMetadataDirectory = $className::modelMetadataDirectory(); | ||
|
|
||
| // Filter models that meet requirements | ||
| $matchingModels = []; | ||
| foreach ($modelMetadataDirectory->listModelMetadata() as $modelMetadata) { | ||
| if ($modelMetadata->meetsRequirements($modelRequirements)) { | ||
| $matchingModels[] = $modelMetadata; | ||
| } | ||
| } | ||
|
|
||
| return $matchingModels; | ||
| } | ||
|
|
||
| /** | ||
| * Gets a configured model instance from a provider. | ||
| * | ||
| * @since n.e.x.t | ||
| * | ||
| * @param string $idOrClassName The provider ID or class name. | ||
Ref34t marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * @param string $modelId The model identifier. | ||
| * @param ModelConfig|null $modelConfig The model configuration. | ||
| * @return ModelInterface The configured model instance. | ||
| * @throws InvalidArgumentException If provider or model is not found. | ||
| */ | ||
| public function getProviderModel( | ||
| string $idOrClassName, | ||
| string $modelId, | ||
| ?ModelConfig $modelConfig = null | ||
| ): ModelInterface { | ||
| $className = $this->resolveProviderClassName($idOrClassName); | ||
|
|
||
| // Use static method from ProviderInterface | ||
| /** @var class-string<ProviderInterface> $className */ | ||
| return $className::model($modelId, $modelConfig); | ||
| } | ||
|
|
||
| /** | ||
| * Gets the class name for a registered provider (handles both ID and class name input). | ||
| * | ||
| * @param string $idOrClassName The provider ID or class name. | ||
| * @return string The provider class name. | ||
Ref34t marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * @throws InvalidArgumentException If provider is not registered. | ||
| */ | ||
| private function resolveProviderClassName(string $idOrClassName): string | ||
| { | ||
| // Handle both ID and class name | ||
| $className = $this->providerClassNames[$idOrClassName] ?? $idOrClassName; | ||
|
|
||
| if (!$this->hasProvider($idOrClassName)) { | ||
| throw new InvalidArgumentException( | ||
| sprintf('Provider not registered: %s', $idOrClassName) | ||
| ); | ||
| } | ||
|
|
||
| return $className; | ||
| } | ||
| } | ||
Ref34t marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.