Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ abstract class AbstractApiBasedModelMetadataDirectory implements
*/
private const MODELS_CACHE_KEY = 'models';

/**
* Request-local cache for explicit model metadata lookups.
*
* @since n.e.x.t
*
* @var array<string, ModelMetadata|null>
*/
private array $explicitModelMetadataCache = [];

/**
* {@inheritDoc}
*
Expand All @@ -47,7 +56,7 @@ abstract class AbstractApiBasedModelMetadataDirectory implements
final public function listModelMetadata(): array
{
$modelsMetadata = $this->getModelMetadataMap();
return array_values($modelsMetadata);
return array_values($this->applyExplicitModelMetadataOverrides($modelsMetadata));
}

/**
Expand All @@ -57,6 +66,10 @@ final public function listModelMetadata(): array
*/
final public function hasModelMetadata(string $modelId): bool
{
if ($this->getExplicitModelMetadata($modelId) !== null) {
return true;
}

$modelsMetadata = $this->getModelMetadataMap();
return isset($modelsMetadata[$modelId]);
}
Expand All @@ -68,6 +81,11 @@ final public function hasModelMetadata(string $modelId): bool
*/
final public function getModelMetadata(string $modelId): ModelMetadata
{
$explicitModelMetadata = $this->getExplicitModelMetadata($modelId);
if ($explicitModelMetadata !== null) {
return $explicitModelMetadata;
}

$modelsMetadata = $this->getModelMetadataMap();
if (!isset($modelsMetadata[$modelId])) {
throw new InvalidArgumentException(
Expand All @@ -77,6 +95,43 @@ final public function getModelMetadata(string $modelId): ModelMetadata
return $modelsMetadata[$modelId];
}

/**
* Applies explicit metadata overrides to listed model metadata.
*
* @since n.e.x.t
*
* @param array<string, ModelMetadata> $modelsMetadata Map of model ID to model metadata.
* @return array<string, ModelMetadata> Map of model ID to model metadata with explicit overrides applied.
*/
private function applyExplicitModelMetadataOverrides(array $modelsMetadata): array
{
foreach (array_keys($modelsMetadata) as $modelId) {
$explicitModelMetadata = $this->getExplicitModelMetadata($modelId);
if ($explicitModelMetadata !== null) {
$modelsMetadata[$modelId] = $explicitModelMetadata;
}
}

return $modelsMetadata;
}

/**
* Gets explicit model metadata using request-local memoization.
*
* @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.
*/
private function getExplicitModelMetadata(string $modelId): ?ModelMetadata
{
if (!array_key_exists($modelId, $this->explicitModelMetadataCache)) {
$this->explicitModelMetadataCache[$modelId] = $this->createModelMetadataForExplicitModelId($modelId);
}

return $this->explicitModelMetadataCache[$modelId];
}

/**
* Returns the map of model ID to model metadata for all models from the provider.
*
Expand Down Expand Up @@ -114,6 +169,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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,6 +30,13 @@ protected function setUp(): void
];
}

protected function tearDown(): void
{
AiClient::setCache(null);

parent::tearDown();
}

/**
* Tests listModelMetadata() method.
*
Expand Down Expand Up @@ -69,6 +78,150 @@ 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 explicit model metadata is preferred over cached metadata.
*
* @return void
*/
public function testGetModelMetadataPrefersExplicitMetadataOverCachedMetadata(): 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($explicitModelMetadata, $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 hasModelMetadata() returns true for explicit metadata without listing models.
*
* @return void
*/
public function testHasModelMetadataReturnsTrueForExplicitMetadataWithoutListingModels(): void
{
$explicitModelMetadata = $this->createStub(ModelMetadata::class);
$explicitModelMetadata->method('getId')->willReturn('explicit-model');
$directory = new MockApiBasedModelMetadataDirectory([], $explicitModelMetadata);

$this->assertTrue($directory->hasModelMetadata('explicit-model'));
$this->assertSame(0, $directory->getListRequestCount());
}

/**
* Tests listModelMetadata() applies explicit metadata overrides to listed models.
*
* @return void
*/
public function testListModelMetadataAppliesExplicitMetadataOverrides(): void
{
$listedModelMetadata = $this->createStub(ModelMetadata::class);
$listedModelMetadata->method('getId')->willReturn('explicit-model');

$explicitModelMetadata = $this->createStub(ModelMetadata::class);
$explicitModelMetadata->method('getId')->willReturn('explicit-model');

$directory = new MockApiBasedModelMetadataDirectory(
['explicit-model' => $listedModelMetadata],
$explicitModelMetadata
);

$models = $directory->listModelMetadata();

$this->assertCount(1, $models);
$this->assertSame($explicitModelMetadata, $models[0]);
$this->assertSame($explicitModelMetadata, $directory->getModelMetadata('explicit-model'));
$this->assertSame(1, $directory->getExplicitModelMetadataLookupCount());
}

/**
* Tests listModelMetadata() applies explicit metadata overrides to cached models.
*
* @return void
*/
public function testListModelMetadataAppliesExplicitMetadataOverridesToCachedModels(): 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);

$models = $directory->listModelMetadata();

$this->assertCount(1, $models);
$this->assertSame($explicitModelMetadata, $models[0]);
$this->assertSame(0, $directory->getListRequestCount());
}

/**
* Tests explicit model metadata is memoized for a directory instance.
*
* @return void
*/
public function testExplicitModelMetadataIsMemoized(): 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($explicitModelMetadata, $directory->getModelMetadata('explicit-model'));
$this->assertTrue($directory->hasModelMetadata('explicit-model'));
$this->assertSame(1, $directory->getExplicitModelMetadataLookupCount());
$this->assertSame(0, $directory->getListRequestCount());
}

/**
* Tests getModelMetadata() method with non-existent model.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,73 @@ class MockApiBasedModelMetadataDirectory extends AbstractApiBasedModelMetadataDi
*/
private array $mockModels;

/**
* @var ModelMetadata|null
*/
private ?ModelMetadata $explicitModelMetadata;

/**
* @var int
*/
private int $listRequestCount = 0;

/**
* @var int
*/
private int $explicitModelMetadataLookupCount = 0;

/**
* Constructor.
*
* @param array<string, ModelMetadata> $mockModels
*/
public function __construct(array $mockModels = [])
public function __construct(array $mockModels = [], ?ModelMetadata $explicitModelMetadata = null)
{
$this->mockModels = $mockModels;
$this->explicitModelMetadata = $explicitModelMetadata;
}

/**
* @inheritdoc
*/
protected function sendListModelsRequest(): array
{
++$this->listRequestCount;

return $this->mockModels;
}

/**
* @inheritdoc
*/
protected function createModelMetadataForExplicitModelId(string $modelId): ?ModelMetadata
{
++$this->explicitModelMetadataLookupCount;

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;
}

/**
* Returns the number of explicit model metadata lookups.
*
* @return int
*/
public function getExplicitModelMetadataLookupCount(): int
{
return $this->explicitModelMetadataLookupCount;
}
}
Loading