Skip to content

Adds caching support via PSR-16#157

Merged
felixarntz merged 6 commits intotrunkfrom
caching-support
Jan 20, 2026
Merged

Adds caching support via PSR-16#157
felixarntz merged 6 commits intotrunkfrom
caching-support

Conversation

@JasonTheAdams
Copy link
Member

@JasonTheAdams JasonTheAdams commented Dec 30, 2025

Resolves #141

This PR adds optional PSR-16 caching support to the AI Client. When a cache is configured, model metadata from provider APIs (like the list of available models) is stored and reused instead of making repeated API calls. This improves performance and reduces API usage, especially useful in applications that frequently initialize the client or query model availability.

What's new:

  • AiClient::setCache() and AiClient::getCache() for configuring a PSR-16 cache
  • psr/simple-cache added as a dependency
  • AbstractOpenAiCompatibleModelMetadataDirectory now caches /models API responses
  • Cache keys are unique per provider to prevent data conflicts
  • MockCache test helper for verifying cache behavior

@JasonTheAdams JasonTheAdams self-assigned this Dec 30, 2025
@github-actions
Copy link

github-actions bot commented Dec 30, 2025

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: JasonTheAdams <[email protected]>
Co-authored-by: felixarntz <[email protected]>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@JasonTheAdams JasonTheAdams added this to the 0.4.0 milestone Dec 31, 2025
Copy link
Member

@felixarntz felixarntz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JasonTheAdams Looks great! A few additional points to consider, but this is close.

*/
protected function getCacheKey(): string
{
return 'ai_client_models_' . md5(static::class);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great if we could include the current AI Client version in the cache key. A version update may come with an updated DTO shape at some point, so it's always safest to invalidate all caches upon update. Or there may be a fix in a specific ModelMetadataDirectory implementation.

This was relevant quite a few times while I worked on AI Services, so I suggest we incorporate that here too.

We don't store the current version anywhere in the codebase, but it's easy to do that in a maintainable way. We could have a specific constant somewhere which we replace its value for as part of the release process. It could even be via a simple vibe coded script.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an AiClient::VERSION. It seemed like a sensible place to put it since this a client package and that's the main class. How does that sound?

Tentatively resolved in 5ccaee3


// Store in cache.
if ($cache !== null) {
$cache->set($cacheKey, $this->dehydrateModelMetadataMap($modelMetadataMap));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the TTL? We'll need a mechanism for the cache to invalidate.

My suggestion is to use a mix of both:

  • Set a reasonable TTL, e.g. for a couple hours, maybe a day.
  • Add some public method to invalidate the cache. We could introduce an interface like WithCachedDataInterface that has a single method invalidateCaches() that would invalidate all relevant caches for the class. And our ModelMetadataDirectory implementations for cloud providers could make use of that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the trait in 3981997 to handle the invalidateCaches() method.

I also set a 1-day TTL. 👍

Copy link
Member

@felixarntz felixarntz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JasonTheAdams Almost good to go, only one important architectural consideration.

@felixarntz
Copy link
Member

felixarntz commented Jan 18, 2026

@JasonTheAdams Since you're adding the VERSION constant here, I thought it'd be a good idea to document the prepublish requirements, as basic as they are, since now they are becoming slightly less basic. See #172.

(In the future it would be great to automate this, but I think for now this is useful to at least document.)

Copy link
Member

@felixarntz felixarntz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JasonTheAdams LGTM, just one thing that I think is worth improving. Please check before merging, feel free to merge after updating, or let me know if you disagree.

Comment on lines +112 to 115
protected function getBaseCacheKey(): string
{
return 'ai_client_' . AiClient::VERSION . '_' . md5(static::class);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not put this in the trait? This implementation here does nothing that wouldn't be suitable for the trait itself - it's not tied to this specific class.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about this, but I didn't want to assume that we always need the class on the end of the key. We really only need it here because this is an abstract class. But another class may not want that in the key, which would also also keys to be shared between contexts.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it doesn't always need to be like that, but a protected method would still allow changing it. So IMO having this implementation in the trait as "works for ~90% of cases" would still be helpful.

But not a big deal, we can also iterate later, as changing that would not break anything.

@felixarntz felixarntz merged commit a65b26a into trunk Jan 20, 2026
12 of 13 checks passed
@felixarntz felixarntz deleted the caching-support branch January 20, 2026 01:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for caching using PSR-16

2 participants