diff --git a/src/Providers/ApiBasedImplementation/AbstractApiBasedModelMetadataDirectory.php b/src/Providers/ApiBasedImplementation/AbstractApiBasedModelMetadataDirectory.php index 52c0cd5f..1666183d 100644 --- a/src/Providers/ApiBasedImplementation/AbstractApiBasedModelMetadataDirectory.php +++ b/src/Providers/ApiBasedImplementation/AbstractApiBasedModelMetadataDirectory.php @@ -68,6 +68,18 @@ final public function hasModelMetadata(string $modelId): bool */ final public function getModelMetadata(string $modelId): ModelMetadata { + if ($this->hasCache(self::MODELS_CACHE_KEY)) { + $modelsMetadata = $this->getModelMetadataMap(); + if (isset($modelsMetadata[$modelId])) { + return $modelsMetadata[$modelId]; + } + } + + $explicitModelMetadata = $this->createModelMetadataForExplicitModelId($modelId); + if ($explicitModelMetadata !== null) { + return $explicitModelMetadata; + } + $modelsMetadata = $this->getModelMetadataMap(); if (!isset($modelsMetadata[$modelId])) { throw new InvalidArgumentException( @@ -114,6 +126,22 @@ protected function getBaseCacheKey(): string return 'ai_client_' . AiClient::VERSION . '_' . md5(static::class); } + /** + * Creates metadata for an explicit model ID without listing provider models. + * + * Providers whose APIs accept arbitrary/current model IDs can override this to avoid a live list-models request + * when callers already know the model ID they want to instantiate. + * + * @since n.e.x.t + * + * @param string $modelId The explicit model ID. + * @return ModelMetadata|null The model metadata, or null to fall back to listing provider models. + */ + protected function createModelMetadataForExplicitModelId(string $modelId): ?ModelMetadata + { + return null; + } + /** * Sends the API request to list models from the provider and returns the map of model ID to model metadata. * diff --git a/tests/unit/Providers/ApiBasedImplementation/AbstractApiBasedModelMetadataDirectoryTest.php b/tests/unit/Providers/ApiBasedImplementation/AbstractApiBasedModelMetadataDirectoryTest.php index 0313453c..f8a8204c 100644 --- a/tests/unit/Providers/ApiBasedImplementation/AbstractApiBasedModelMetadataDirectoryTest.php +++ b/tests/unit/Providers/ApiBasedImplementation/AbstractApiBasedModelMetadataDirectoryTest.php @@ -6,7 +6,9 @@ use InvalidArgumentException; use PHPUnit\Framework\TestCase; +use WordPress\AiClient\AiClient; use WordPress\AiClient\Providers\Models\DTO\ModelMetadata; +use WordPress\AiClient\Tests\mocks\MockCache; /** * @covers \WordPress\AiClient\Providers\ApiBasedImplementation\AbstractApiBasedModelMetadataDirectory @@ -28,6 +30,13 @@ protected function setUp(): void ]; } + protected function tearDown(): void + { + AiClient::setCache(null); + + parent::tearDown(); + } + /** * Tests listModelMetadata() method. * @@ -69,6 +78,65 @@ public function testGetModelMetadata(): void $this->assertSame($this->mockModels['model-1'], $directory->getModelMetadata('model-1')); } + /** + * Tests getModelMetadata() returns explicit metadata without listing models. + * + * @return void + */ + public function testGetModelMetadataReturnsExplicitMetadataWithoutListingModels(): void + { + $explicitModelMetadata = $this->createStub(ModelMetadata::class); + $explicitModelMetadata->method('getId')->willReturn('explicit-model'); + $directory = new MockApiBasedModelMetadataDirectory([], $explicitModelMetadata); + + $this->assertSame($explicitModelMetadata, $directory->getModelMetadata('explicit-model')); + $this->assertSame(0, $directory->getListRequestCount()); + } + + /** + * Tests cached model metadata is preferred over explicit metadata. + * + * @return void + */ + public function testGetModelMetadataPrefersCachedMetadataOverExplicitMetadata(): void + { + $cache = new MockCache(); + $cacheKey = 'ai_client_' . AiClient::VERSION . '_' . md5(MockApiBasedModelMetadataDirectory::class) . '_models'; + $cachedModelMetadata = $this->createStub(ModelMetadata::class); + $cachedModelMetadata->method('getId')->willReturn('explicit-model'); + $cache->seed($cacheKey, ['explicit-model' => $cachedModelMetadata]); + AiClient::setCache($cache); + + $explicitModelMetadata = $this->createStub(ModelMetadata::class); + $explicitModelMetadata->method('getId')->willReturn('explicit-model'); + $directory = new MockApiBasedModelMetadataDirectory([], $explicitModelMetadata); + + $this->assertSame($cachedModelMetadata, $directory->getModelMetadata('explicit-model')); + $this->assertSame(0, $directory->getListRequestCount()); + } + + /** + * Tests cached metadata misses can still fall back to explicit metadata. + * + * @return void + */ + public function testGetModelMetadataUsesExplicitMetadataAfterCachedMetadataMiss(): void + { + $cache = new MockCache(); + $cacheKey = 'ai_client_' . AiClient::VERSION . '_' . md5(MockApiBasedModelMetadataDirectory::class) . '_models'; + $otherModelMetadata = $this->createStub(ModelMetadata::class); + $otherModelMetadata->method('getId')->willReturn('other-model'); + $cache->seed($cacheKey, ['other-model' => $otherModelMetadata]); + AiClient::setCache($cache); + + $explicitModelMetadata = $this->createStub(ModelMetadata::class); + $explicitModelMetadata->method('getId')->willReturn('explicit-model'); + $directory = new MockApiBasedModelMetadataDirectory([], $explicitModelMetadata); + + $this->assertSame($explicitModelMetadata, $directory->getModelMetadata('explicit-model')); + $this->assertSame(0, $directory->getListRequestCount()); + } + /** * Tests getModelMetadata() method with non-existent model. * diff --git a/tests/unit/Providers/ApiBasedImplementation/MockApiBasedModelMetadataDirectory.php b/tests/unit/Providers/ApiBasedImplementation/MockApiBasedModelMetadataDirectory.php index 3635c069..6b5bf518 100644 --- a/tests/unit/Providers/ApiBasedImplementation/MockApiBasedModelMetadataDirectory.php +++ b/tests/unit/Providers/ApiBasedImplementation/MockApiBasedModelMetadataDirectory.php @@ -17,14 +17,25 @@ class MockApiBasedModelMetadataDirectory extends AbstractApiBasedModelMetadataDi */ private array $mockModels; + /** + * @var ModelMetadata|null + */ + private ?ModelMetadata $explicitModelMetadata; + + /** + * @var int + */ + private int $listRequestCount = 0; + /** * Constructor. * * @param array $mockModels */ - public function __construct(array $mockModels = []) + public function __construct(array $mockModels = [], ?ModelMetadata $explicitModelMetadata = null) { $this->mockModels = $mockModels; + $this->explicitModelMetadata = $explicitModelMetadata; } /** @@ -32,6 +43,30 @@ public function __construct(array $mockModels = []) */ protected function sendListModelsRequest(): array { + ++$this->listRequestCount; + return $this->mockModels; } + + /** + * @inheritdoc + */ + protected function createModelMetadataForExplicitModelId(string $modelId): ?ModelMetadata + { + if ($this->explicitModelMetadata !== null && $this->explicitModelMetadata->getId() === $modelId) { + return $this->explicitModelMetadata; + } + + return parent::createModelMetadataForExplicitModelId($modelId); + } + + /** + * Returns the number of list request callbacks. + * + * @return int + */ + public function getListRequestCount(): int + { + return $this->listRequestCount; + } }