Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1479dc5
feat: adds tools for importing PHP AI Client
JasonTheAdams Feb 5, 2026
2c842f1
feat: adds php-ai-client to includes
JasonTheAdams Feb 5, 2026
99efae9
feat: adds ai client
JasonTheAdams Feb 6, 2026
1c07c3e
refactor: moves prompt builder and renames directory
JasonTheAdams Feb 6, 2026
23f1af0
fix: handles support methods in an error state
JasonTheAdams Feb 6, 2026
42197b5
refactor: namespaces PSR classes and corrects versions
JasonTheAdams Feb 6, 2026
8a9d2c6
feat: adds wp_ai_client_prompt function
JasonTheAdams Feb 7, 2026
56c6873
refactor: corrects formatting issues
JasonTheAdams Feb 7, 2026
a5bd792
refactor: adds and runs third-party tree-shaking
JasonTheAdams Feb 11, 2026
242f9f9
test: corrects PHP 8.5 compatibility
JasonTheAdams Feb 11, 2026
7caa159
test: corrects formatting issues
JasonTheAdams Feb 11, 2026
0edbfef
test: adjusts reflection accessibility for PHP compatibility
JasonTheAdams Feb 11, 2026
ebbdc54
refactor: removes unnecessary polyfills
JasonTheAdams Feb 11, 2026
278f753
fix: adds missing translation functions and comments
JasonTheAdams Feb 11, 2026
85b1916
test: adds ai client util tests
JasonTheAdams Feb 11, 2026
0e78c62
Adjust path filtering for code coverage reports.
desrosj Feb 12, 2026
1e2d52c
Merge branch 'trunk' into add/wp-ai-client
felixarntz Feb 14, 2026
9626b32
chore: explicitly lays out $prompt types
JasonTheAdams Feb 16, 2026
62b33aa
feat: locks in only supported stream from file mode
JasonTheAdams Feb 19, 2026
52d4963
refactor: simplifies to using array_find
JasonTheAdams Feb 19, 2026
472f69f
test: adds Prompt_Builder snake case test
JasonTheAdams Feb 20, 2026
b4f9bd7
test: fixes using_abilities tests
JasonTheAdams Feb 20, 2026
9c3b25e
fix: correctly identifies falsey cached values
JasonTheAdams Feb 20, 2026
d708bd2
refactor: moves function resolver and renames ai client folder
JasonTheAdams Feb 20, 2026
97f8598
refactor: switches to wp_safe_remote_request
JasonTheAdams Feb 20, 2026
9e0ebcc
refactor: uses str_starts_with to simplify
JasonTheAdams Feb 20, 2026
76bc7ba
Merge branch 'trunk' into add/wp-ai-client
felixarntz Feb 20, 2026
9364aca
Move AI client initialization out of require file block.
felixarntz Feb 20, 2026
87c0400
Add correct ticket annotations for tests.
felixarntz Feb 20, 2026
d97a86a
Translate messages from failed ability lookup or execution.
felixarntz Feb 20, 2026
582d0d0
Remove unnecessary X-Stream header exclusion.
felixarntz Feb 20, 2026
9098a6e
Yoda.
felixarntz Feb 20, 2026
5f3c5be
Mark infra classes as private.
felixarntz Feb 20, 2026
00ef1b8
Clean up abilities during testing.
felixarntz Feb 20, 2026
0e8a5dc
Make function_name_to_ability_name public.
felixarntz Feb 20, 2026
e8a2a2f
Add warning if invalid ability slug is passed to using_abilities.
felixarntz Feb 20, 2026
0774f6f
feat: always clones php ai client
JasonTheAdams Feb 20, 2026
c58351d
feat: updates PHP AI Client to 1.1.0
JasonTheAdams Feb 20, 2026
7f94810
refactor: uses new PHP AI Client http discovery system
JasonTheAdams Feb 20, 2026
76af7a8
test: fixes failing test
JasonTheAdams Feb 20, 2026
b5baa00
refactor: moves all ai-client classes into ai-client directory
JasonTheAdams Feb 20, 2026
6fa12cb
chore: adds php ai client tool readme
JasonTheAdams Feb 20, 2026
c6880e1
Revert temporary change to see full test coverage.
felixarntz Feb 20, 2026
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
1 change: 1 addition & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
<exclude-pattern>/src/wp-includes/js/*</exclude-pattern>
<exclude-pattern>/src/wp-includes/PHPMailer/*</exclude-pattern>
<exclude-pattern>/src/wp-includes/Requests/*</exclude-pattern>
<exclude-pattern>/src/wp-includes/php-ai-client/*</exclude-pattern>
<exclude-pattern>/src/wp-includes/SimplePie/*</exclude-pattern>
<exclude-pattern>/src/wp-includes/sodium_compat/*</exclude-pattern>
<exclude-pattern>/src/wp-includes/Text/*</exclude-pattern>
Expand Down
1 change: 1 addition & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
<directory suffix=".php">src/wp-includes/IXR</directory>
<directory suffix=".php">src/wp-includes/PHPMailer</directory>
<directory suffix=".php">src/wp-includes/Requests</directory>
<directory suffix=".php">src/wp-includes/php-ai-client</directory>
<directory suffix=".php">src/wp-includes/SimplePie</directory>
<directory suffix=".php">src/wp-includes/sodium_compat</directory>
<directory suffix=".php">src/wp-includes/Text</directory>
Expand Down
34 changes: 34 additions & 0 deletions src/wp-includes/ai-client.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php
/**
* WordPress AI Client API.
*
* @package WordPress
* @subpackage AI
* @since 7.0.0
*/

use WordPress\AiClient\AiClient;

/**
* Creates a new AI prompt builder using the default provider registry.
*
* This is the main entry point for generating AI content in WordPress. It returns
* a fluent builder that can be used to configure and execute AI prompts.
*
* The prompt can be provided as a simple string for basic text prompts, or as more
* complex types for advanced use cases like multi-modal content or conversation history.
*
* @since 7.0.0
*
* @param string|MessagePart|Message|array|list<string|MessagePart|array>|list<Message>|null $prompt Optional. Initial prompt content.
* A string for simple text prompts,
* a MessagePart or Message object for
* structured content, an array for a
* message array shape, or a list of
* parts or messages for multi-turn
* conversations. Default null.
* @return WP_AI_Client_Prompt_Builder The prompt builder instance.
*/
function wp_ai_client_prompt( $prompt = null ) {
return new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry(), $prompt );
}
216 changes: 216 additions & 0 deletions src/wp-includes/ai-client/adapters/class-wp-ai-client-cache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
<?php
/**
* WP AI Client: WP_AI_Client_Cache class
*
* @package WordPress
* @subpackage AI
* @since 7.0.0
*/

use WordPress\AiClientDependencies\Psr\SimpleCache\CacheInterface;

/**
* WordPress-specific PSR-16 cache adapter for the AI Client.
*
* Bridges PSR-16 cache operations to WordPress object cache functions,
* enabling the AI client to leverage WordPress caching infrastructure.
*
* @since 7.0.0
* @internal Intended only to wire up the PHP AI Client SDK to WordPress's caching system.
* @access private
*/
class WP_AI_Client_Cache implements CacheInterface {
Copy link
Member

Choose a reason for hiding this comment

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

Thinking out loud, would it be a bit better to call that directory ai-client-implementations or ai-client-infrastructure or ai-client-internal? Better than the very generic utils name, and "utils" may suggest that it's utilities that people are encouraged to use.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I've not loved the name. Abilities API and other simply use abilities-api and such, putting the main public classes in wp-includes. So, by precedent, I think we'd just call this ai-client and keep it simple.

Copy link
Member Author

Choose a reason for hiding this comment

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

Tentatively resolved in d708bd2


/**
* Cache group used for all cache operations.
*
* @since 7.0.0
* @var string
*/
private const CACHE_GROUP = 'wp_ai_client';

/**
* Fetches a value from the cache.
*
* @since 7.0.0
*
* @param string $key The unique key of this item in the cache.
* @param mixed $default_value Default value to return if the key does not exist.
* @return mixed The value of the item from the cache, or $default_value in case of cache miss.
*/
public function get( $key, $default_value = null ) {
$found = false;
$value = wp_cache_get( $key, self::CACHE_GROUP, false, $found );

if ( ! $found ) {
return $default_value;
}

return $value;
}

/**
* Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time.
*
* @since 7.0.0
*
* @param string $key The key of the item to store.
* @param mixed $value The value of the item to store, must be serializable.
* @param null|int|DateInterval $ttl Optional. The TTL value of this item.
* @return bool True on success and false on failure.
*/
public function set( $key, $value, $ttl = null ): bool {
$expire = $this->ttl_to_seconds( $ttl );

return wp_cache_set( $key, $value, self::CACHE_GROUP, $expire );
}

/**
* Delete an item from the cache by its unique key.
*
* @since 7.0.0
*
* @param string $key The unique cache key of the item to delete.
* @return bool True if the item was successfully removed. False if there was an error.
*/
public function delete( $key ): bool {
return wp_cache_delete( $key, self::CACHE_GROUP );
}

/**
* Wipes clean the entire cache's keys.
*
* This method only clears the cache group used by this adapter. If the underlying
* cache implementation does not support group flushing, this method returns false.
*
* @since 7.0.0
*
* @return bool True on success and false on failure.
*/
public function clear(): bool {
if ( ! function_exists( 'wp_cache_supports' ) || ! wp_cache_supports( 'flush_group' ) ) {
return false;
}

return wp_cache_flush_group( self::CACHE_GROUP );
}

/**
* Obtains multiple cache items by their unique keys.
*
* @since 7.0.0
*
* @param iterable<string> $keys A list of keys that can be obtained in a single operation.
* @param mixed $default_value Default value to return for keys that do not exist.
* @return array<string, mixed> A list of key => value pairs.
*/
public function getMultiple( $keys, $default_value = null ) {
/**
* Keys array.
*
* @var array<string> $keys_array
*/
$keys_array = $this->iterable_to_array( $keys );
$values = wp_cache_get_multiple( $keys_array, self::CACHE_GROUP );
$result = array();

foreach ( $keys_array as $key ) {
if ( false === $values[ $key ] ) {
// Could be a stored false or a cache miss — disambiguate via get().
$result[ $key ] = $this->get( $key, $default_value );
} else {
$result[ $key ] = $values[ $key ];
}
}

return $result;
}

/**
* Persists a set of key => value pairs in the cache, with an optional TTL.
*
* @since 7.0.0
*
* @param iterable<string, mixed> $values A list of key => value pairs for a multiple-set operation.
* @param null|int|DateInterval $ttl Optional. The TTL value of this item.
* @return bool True on success and false on failure.
*/
public function setMultiple( $values, $ttl = null ): bool {
$values_array = $this->iterable_to_array( $values );
$expire = $this->ttl_to_seconds( $ttl );
$results = wp_cache_set_multiple( $values_array, self::CACHE_GROUP, $expire );

// Return true only if all operations succeeded.
return ! in_array( false, $results, true );
}

/**
* Deletes multiple cache items in a single operation.
*
* @since 7.0.0
*
* @param iterable<string> $keys A list of string-based keys to be deleted.
* @return bool True if the items were successfully removed. False if there was an error.
*/
public function deleteMultiple( $keys ): bool {
$keys_array = $this->iterable_to_array( $keys );
$results = wp_cache_delete_multiple( $keys_array, self::CACHE_GROUP );

// Return true only if all operations succeeded.
return ! in_array( false, $results, true );
}

/**
* Determines whether an item is present in the cache.
*
* @since 7.0.0
*
* @param string $key The cache item key.
* @return bool True if the item exists in the cache, false otherwise.
*/
public function has( $key ): bool {
$found = false;
wp_cache_get( $key, self::CACHE_GROUP, false, $found );

return (bool) $found;
}

/**
* Converts a PSR-16 TTL value to seconds for WordPress cache functions.
*
* @since 7.0.0
*
* @param null|int|DateInterval $ttl The TTL value.
* @return int The TTL in seconds, or 0 for no expiration.
*/
private function ttl_to_seconds( $ttl ): int {
if ( null === $ttl ) {
return 0;
}

if ( $ttl instanceof DateInterval ) {
$now = new DateTime();
$end = ( clone $now )->add( $ttl );

return $end->getTimestamp() - $now->getTimestamp();
}

return max( 0, (int) $ttl );
}

/**
* Converts an iterable to an array.
*
* @since 7.0.0
*
* @param iterable<mixed> $items The iterable to convert.
* @return array<mixed> The array.
*/
private function iterable_to_array( $items ): array {
if ( is_array( $items ) ) {
return $items;
}

return iterator_to_array( $items );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
/**
* WP AI Client: WP_AI_Client_Discovery_Strategy class
*
* @package WordPress
* @subpackage AI
* @since 7.0.0
*/

use WordPress\AiClient\Providers\Http\Abstracts\AbstractClientDiscoveryStrategy;
use WordPress\AiClientDependencies\Nyholm\Psr7\Factory\Psr17Factory;
use WordPress\AiClientDependencies\Psr\Http\Client\ClientInterface;

/**
* Discovery strategy for WordPress HTTP client.
*
* Registers the WordPress HTTP client adapter with the HTTPlug discovery system
* so the AI Client SDK can find and use it automatically.
*
* @since 7.0.0
* @internal Intended only to register WordPress's HTTP client so that the PHP AI Client SDK can use it.
* @access private
*/
class WP_AI_Client_Discovery_Strategy extends AbstractClientDiscoveryStrategy {

/**
* Creates an instance of the WordPress HTTP client.
*
* @since 7.0.0
*
* @param Psr17Factory $psr17_factory The PSR-17 factory for creating HTTP messages.
* @return ClientInterface The PSR-18 HTTP client.
*/
protected static function createClient( Psr17Factory $psr17_factory ): ClientInterface {
return new WP_AI_Client_HTTP_Client( $psr17_factory, $psr17_factory );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php
/**
* WP AI Client: WP_AI_Client_Event_Dispatcher class
*
* @package WordPress
* @subpackage AI
* @since 7.0.0
*/

use WordPress\AiClientDependencies\Psr\EventDispatcher\EventDispatcherInterface;

/**
* WordPress-specific PSR-14 event dispatcher for the AI Client.
*
* Bridges PSR-14 events to WordPress action hooks, enabling plugins to hook
* into AI client lifecycle events.
*
* @since 7.0.0
* @internal Intended only to wire up the PHP AI Client SDK to WordPress's hook system.
* @access private
*/
class WP_AI_Client_Event_Dispatcher implements EventDispatcherInterface {

/**
* Dispatches an event to WordPress action hooks.
*
* Converts the event class name to a WordPress action hook name and fires it.
* For example, BeforeGenerateResultEvent becomes wp_ai_client_before_generate_result.
*
* @since 7.0.0
*
* @param object $event The event object to dispatch.
* @return object The same event object, potentially modified by listeners.
*/
public function dispatch( object $event ): object {
$event_name = $this->get_hook_name_portion_for_event( $event );

/**
* Fires when an AI client event is dispatched.
*
* The dynamic portion of the hook name, `$event_name`, refers to the
* snake_case version of the event class name, without the `_event` suffix.
*
* For example, an event class named `BeforeGenerateResultEvent` will fire the
* `wp_ai_client_before_generate_result` action hook.
*
* In practice, the available action hook names are:
*
* - wp_ai_client_before_generate_result
* - wp_ai_client_after_generate_result
*
* @since 7.0.0
*
* @param object $event The event object.
*/
do_action( "wp_ai_client_{$event_name}", $event );

return $event;
}

/**
* Converts an event object class name to a WordPress action hook name portion.
*
* @since 7.0.0
*
* @param object $event The event object.
* @return string The hook name portion derived from the event class name.
*/
private function get_hook_name_portion_for_event( object $event ): string {
$class_name = get_class( $event );
$pos = strrpos( $class_name, '\\' );
$short_name = false !== $pos ? substr( $class_name, $pos + 1 ) : $class_name;

// Convert PascalCase to snake_case.
$snake_case = strtolower( (string) preg_replace( '/([a-z])([A-Z])/', '$1_$2', $short_name ) );

// Strip '_event' suffix if present.
if ( str_ends_with( $snake_case, '_event' ) ) {
$snake_case = (string) substr( $snake_case, 0, -6 );
}

return $snake_case;
}
}
Loading
Loading