Repository navigation
AI: Update PHP AI Client to 1.5.0 and introduce WP_AI_Client_Embedding_Builder - #12530
JasonTheAdams wants to merge 17 commits into
Conversation
Regenerated via `bash tools/php-ai-client/installer.sh --version=1.4.0`. The 1.4.0 release adds embedding generation support, including the new EmbeddingBuilder, embedding lifecycle events, Embedding/EmbeddingResult DTOs, and a shared ModelResolver for model selection. Full changelog: https://github.com/WordPress/php-ai-client/releases/tag/1.4.0 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wraps the PHP AI Client SDK's new EmbeddingBuilder (added in 1.4.0), following the same pattern as WP_AI_Client_Prompt_Builder: snake_case method naming via __call() proxying, and WP_Error handling instead of exceptions, with an error state that preserves the fluent interface. Introduces the wp_ai_client_embedding() entry point function and the wp_ai_client_prevent_embedding filter, analogous to their prompt counterparts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
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 Unlinked AccountsThe following contributors have not linked their GitHub and WordPress.org accounts: @claude, @chubes4, @dugyen. Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases. Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
dkotter
left a comment
There was a problem hiding this comment.
Haven't tested this out yet but overall this looks good to me, noting most of what's here is coming from the PHP AI Client repo so I haven't reviewed any of that closely. Left a few comments that may be worth discussing
In 1.4.0, model selection state (model, registry, providerIdOrClassName, requestOptions) moved from the SDK's PromptBuilder onto its ModelResolver. Update the reflection helper to fall back to the model resolver for properties that no longer live on the wrapped builder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ass. WP_AI_Client_Prompt_Builder and WP_AI_Client_Embedding_Builder shared nearly all of their machinery: the snake_case __call() proxying, the WP_Error error-state handling, exception-to-WP_Error conversion, and the default request timeout setup. Move that into an abstract WP_AI_Client_Builder base class, parameterized via abstract methods for the SDK builder instantiation, error code prefix, prevent filter, and generating/support-check method maps. No behavior changes: error codes, filter names, and _doing_it_wrong notices (which continue to report the concrete class name) are identical to before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… filter. Allows developers to adjust the default request timeout per builder type, e.g. a different timeout for embeddings than for prompts. The builder's class name is passed as the second filter parameter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…edding(). Covers the WordPress wrapper behavior (not the SDK itself): constructor input parsing and the default request timeout handling, snake_case fluent proxying and error-state semantics, the wp_supports_ai() and wp_ai_client_prevent_embedding gates, exception-to-WP_Error mapping with embedding-prefixed error codes, generation through a mock embedding model, and the variadic input handling of wp_ai_client_embedding(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Created the corresponding Core Trac ticket: https://core.trac.wordpress.org/ticket/65638 @JasonTheAdams please add the full ticket URL to this PR description to clear the missing-ticket check. The ticket links this implementation, PHP AI Client #244, and the 1.4.0 release. |
|
Done! Thanks, @chubes4! |
dkotter
left a comment
There was a problem hiding this comment.
Code looks good here to me and I've tested these changes out using the OpenAI Provider (from this PR) and the test WP-CLI command we have in the AI plugin (from this PR, with a few changes to utilize the new wp_ai_client_embedding function). Everything works as I'd expect.
Just as a personal opinion/thought, I think it would be nice to be able to pass in an array of data to get multiple embeddings.
Right now you have to do:
wp_ai_client_embedding( 'first input', 'second input' )->generate_embeddings()As this won't work:
wp_ai_client_embedding( array( 'first input', 'second input' ) )->generate_embeddings()The second feels a bit more natural to me but not something I feel strongly about. Can still use an array as the argument and just spread the result:
wp_ai_client_embedding( ...array( 'first input', 'second input' ) )->generate_embeddings()|
Thanks, @dkotter! My personal preference is to have folks spread the array for variadic parameters, as you showed in your third example. The reason being that it keeps the function signature simple and less prone to bugs. I'm a fan of variadic parameters because PHP doesn't support array generics, so it's a great way to have an array parameter that's strictly typed. |
|
It's now possible to land changes planned for WordPress 7.2. I would be in favor of landing this patch early to allow in-depth testing with plugins that implement the proposed AI interfaces. The first step would be to ensure all code references to WordPress versions are updated to 7.2.0 instead of 7.1.0. |
Test ReportTicket: Trac #65638 Result: PASS, with one carried-forward bug flagged Environment
What the PR doesBumps the bundled PHP AI Client SDK from 1.3.1 to 1.4.0 (adds embedding support), extracts the shared logic of Steps taken
Actual result
Additional notes
Support content1. Reproduce the carried-forward TypeError bug (patched code): $builder = wp_ai_client_embedding( 'hello world' );
$builder->using_dimensions( array( 256 ) ); // fatals: uncaught TypeErrorOutput: 2. Happy-path / regression checks (patched code): $b = wp_ai_client_embedding( 'hello world' );
$b->using_dimensions( 256 ); // fluent, same instance
$b->is_supported(); // false, no fatal
$b->generate_embedding(); // WP_Error: embedding_invalid_argument
add_filter( 'wp_ai_client_prevent_embedding', '__return_true' );
wp_ai_client_embedding( 'hi' )->generate_embedding();
// WP_Error: embedding_prevented, status 503
$p = wp_ai_client_prompt( 'Test prompt' );
$p->using_temperature( 0.7 ); // fluent, same instance
$p->generate_text(); // WP_Error: prompt_invalid_argument
// -> no regression from the WP_AI_Client_Builder refactor🤖 Test performed with Claude Code |
Recommended fix for the carried-forward TypeError bugFollowing up on the test report above — here's a concrete, tested fix for the issue: Applying the exact same pattern PR #12256 used (widen both --- a/src/wp-includes/ai-client/class-wp-ai-client-builder.php
+++ b/src/wp-includes/ai-client/class-wp-ai-client-builder.php
@@ -65,9 +65,9 @@
public function __construct( ProviderRegistry $registry, $input = null ) {
try {
$this->builder = $this->create_sdk_builder( $registry, $input );
- } catch ( Exception $e ) {
+ } catch ( Throwable $e ) {
$this->builder = $this->create_sdk_builder( $registry, null );
- $this->error = $this->exception_to_wp_error( $e );
+ $this->error = $this->throwable_to_wp_error( $e );
}
$default_timeout = 30.0;
@@ -243,8 +243,8 @@
}
return $result;
- } catch ( Exception $e ) {
- $this->error = $this->exception_to_wp_error( $e );
+ } catch ( Throwable $e ) {
+ $this->error = $this->throwable_to_wp_error( $e );
if ( $this->is_generating_method( $name ) ) {
return $this->error;
@@ -254,36 +254,36 @@
}
/**
- * Converts an exception into a WP_Error with a structured error code and message.
+ * Converts a throwable into a WP_Error with a structured error code and message.
*
- * This method maps different exception types to specific WP_Error codes and HTTP status codes.
+ * This method maps different throwable types to specific WP_Error codes and HTTP status codes.
* The presence of the status codes means these WP_Error objects can be easily used in REST API responses
* or other contexts where HTTP semantics are relevant.
*
* @since 7.0.0
* @since 7.1.0 Moved from `WP_AI_Client_Prompt_Builder` to the `WP_AI_Client_Builder` base class.
*
- * @param Exception $e The exception to convert.
+ * @param Throwable $throwable The throwable to convert.
* @return WP_Error The resulting WP_Error object.
*/
- protected function exception_to_wp_error( Exception $e ): WP_Error {
+ protected function throwable_to_wp_error( Throwable $throwable ): WP_Error {
$prefix = $this->get_error_code_prefix();
- if ( $e instanceof NetworkException ) {
+ if ( $throwable instanceof NetworkException ) {
$error_code = $prefix . '_network_error';
$status_code = 503;
- } elseif ( $e instanceof ClientException ) {
+ } elseif ( $throwable instanceof ClientException ) {
// `ClientException` uses HTTP status codes as exception codes, so we can rely on them.
$error_code = $prefix . '_client_error';
- $status_code = $e->getCode() ? $e->getCode() : 400;
- } elseif ( $e instanceof ServerException ) {
+ $status_code = $throwable->getCode() ? $throwable->getCode() : 400;
+ } elseif ( $throwable instanceof ServerException ) {
// `ServerException` uses HTTP status codes as exception codes, so we can rely on them.
$error_code = $prefix . '_upstream_server_error';
- $status_code = $e->getCode() ? $e->getCode() : 500;
- } elseif ( $e instanceof TokenLimitReachedException ) {
+ $status_code = $throwable->getCode() ? $throwable->getCode() : 500;
+ } elseif ( $throwable instanceof TokenLimitReachedException ) {
$error_code = $prefix . '_token_limit_reached';
$status_code = 400;
- } elseif ( $e instanceof InvalidArgumentException ) {
+ } elseif ( $throwable instanceof InvalidArgumentException ) {
$error_code = $prefix . '_invalid_argument';
$status_code = 400;
} else {
@@ -293,10 +293,10 @@
return new WP_Error(
$error_code,
- $e->getMessage(),
+ $throwable->getMessage(),
array(
'status' => $status_code,
- 'exception_class' => get_class( $e ),
+ 'exception_class' => get_class( $throwable ),
)
);
}Verified liveApplied this PR + the fix above to a test install and re-ran the same reproduction from the test report: Both 🤖 Generated with Claude Code |
Resolves conflicts in `WP_AI_Client_Prompt_Builder` by keeping the base class refactor. The trunk fixes to the prompt builder (r63298, r63299, r63346) are ported to `WP_AI_Client_Builder` in the follow-up commit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Av4uVj7v96r2bUKX7kMd8z
The base class extracted from `WP_AI_Client_Prompt_Builder` predated three fixes that landed on trunk for the prompt builder, so they were lost in the refactor and missing from the new embedding builder: * Catch `Throwable` instead of `Exception` in the constructor and `__call()`, so a `TypeError` from the strict-typed SDK (e.g. `using_dimensions( array( 256 ) )`) becomes a `WP_Error` instead of a fatal error. `exception_to_wp_error()` is renamed to `throwable_to_wp_error()` to match. See #65505. * Return `false` from support check methods when the wrapped builder throws. See #65781. * Clone the wrapped builder and any stored error in `__clone()`, so the clone passed to the prevent filters cannot mutate the original. See #65782. Adds matching regression tests for the embedding builder. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Av4uVj7v96r2bUKX7kMd8z
Trunk is now 7.2-alpha, so the embedding builder and base class will ship in 7.2.0. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Av4uVj7v96r2bUKX7kMd8z
Regenerated via `bash tools/php-ai-client/installer.sh --version=1.5.0`. The 1.5.0 release changes `EmbeddingBuilder` to never select a model automatically. Embedding vectors are only comparable to vectors from the same model, so a stored corpus is bound to the model that created it, and automatic selection could silently invalidate existing vectors. A model must now be specified via `usingModel()` or the new `usingProviderModel()`, and `isSupported()` checks that the specified model can fulfill the request. `usingProvider()` and `usingModelPreference()` are no longer available on `EmbeddingBuilder`. Full changelog: https://github.com/WordPress/php-ai-client/releases/tag/1.5.0 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Av4uVj7v96r2bUKX7kMd8z
Follows the PHP AI Client 1.5.0 change that removes automatic model selection from `EmbeddingBuilder`. Updates the documented methods to drop `using_provider()` and `using_model_preference()` and add `using_provider_model()`, and documents that a model is required. Without a model, `is_supported()` returns false and generating methods return an `embedding_invalid_argument` error, through the existing `Throwable` handling in `WP_AI_Client_Builder`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Av4uVj7v96r2bUKX7kMd8z
|
I'm not sure why the tests are failing. They're working for me locally. Seems like there's something going on with the docker containers. |
|
Would love to get this in before Beta 1, can iterate from there if needed |
gziolo
left a comment
There was a problem hiding this comment.
Looks good overall. Two non-blocking questions below about array inputs and SDK subclass compatibility.
| { | ||
| $this->registry = $registry; | ||
| $this->modelConfig = new ModelConfig(); | ||
| $this->modelResolver = new ModelResolver($registry); |
There was a problem hiding this comment.
Does upstream guarantee compatibility for subclasses using the protected properties moved into ModelResolver? For example, assigning $this->model in a subclass previously selected the model, but generation now ignores that assignment. If this usage is supported, should upstream preserve it or document migration to usingModel()?
There was a problem hiding this comment.
Good catch. That was a backward-breaking change, though a deep and internal one. My inclination is that how it is now is "more right" in that it's closed by default. Since we're not subclassing this anywhere in core, and this was an undocumented part of the API, I think we're safe to continue.
|
With the new SDK events the event dispatcher now also fires wp_ai_client_before_generate_embedding and wp_ai_client_after_generate_embedding, but the list of hooks in the docblock of WP_AI_Client_Event_Dispatcher::dispatch still only mentions the generate_result ones @since 7.2.0 note so they show up on the hooks reference.
|
jorgefilipecosta
left a comment
There was a problem hiding this comment.
Left some minor comments for consideration, but nothing is a blocker and things tested well.
I used the open ai connector (after setting a key) and then I used variations of this code to test:
<?php
add_action(
'template_redirect',
static function () {
if ( is_admin() || wp_doing_ajax() || wp_doing_cron() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
return;
}
header( 'Content-Type: text/plain; charset=utf-8' );
$result = wp_ai_client_embedding( 'The quick brown fox', 'jumps over the lazy dog' )
->using_provider_model( 'openai', 'text-embedding-3-small' )
->using_dimensions( 256 )
->generate_embedding_result();
foreach ( $result->getEmbeddings() as $embedding ) {
$vector = $embedding->getValues();
$size = $embedding->getDimensions();
var_dump( $size, count( $vector ), $vector );
}
var_dump( $result->getTokenUsage() );
var_dump( $result->getModelMetadata()->getId() );
die();
}
);
I just loaded the site frontend and inspected the results.
In all the tests I did things worked as expected.
`wp_ai_client_embedding( array( 'First', 'Second' ) )` previously treated the array as a single input and failed with an `embedding_invalid_argument` error. A single list argument is now unwrapped into individual inputs, so callers no longer need to spread an existing array. Passing multiple arguments still works as before. This is unambiguous: the only array that is a valid single input is a message part array shape, which is associative and never a list. The SDK's `EmbeddingBuilder` constructor relies on the same distinction. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Av4uVj7v96r2bUKX7kMd8z
Adds a test that `generate_embedding_result()` fires `wp_ai_client_before_generate_embedding` and `wp_ai_client_after_generate_embedding` in order, mirroring the prompt builder's lifecycle hook test, and lists both hooks in the `WP_AI_Client_Event_Dispatcher::dispatch()` documentation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Av4uVj7v96r2bUKX7kMd8z
Tests added for the embedding builder, `wp_ai_client_embedding()`, and the `wp_ai_client_default_request_timeout` builder type parameter now reference #65638 instead of the original AI client ticket #64591. Regression tests for fixes ported from trunk keep their original ticket alongside it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Av4uVj7v96r2bUKX7kMd8z
`$builder`, `$error` and `throwable_to_wp_error()` were private on `WP_AI_Client_Prompt_Builder` in 7.0 and neither child class uses them, so they are private on the base class too rather than becoming new protected API. `get_builder_callable()` stays protected, as it was in 7.0. The base class is also marked `@internal` and `@access private`, like the AI client adapters, so plugins don't extend it and abstract methods can be added later without breaking subclasses. Tests now reflect on `WP_AI_Client_Builder` to reach these members. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Av4uVj7v96r2bUKX7kMd8z
The throwable mapping test no longer invokes the private `throwable_to_wp_error()` through reflection. The mock model now throws each throwable from `generateEmbeddingResult()`, and the test asserts on the `WP_Error` that `generate_embedding_result()` returns. Removes two tests that are now redundant: * `test_is_supported_returns_false_when_builder_throws` swapped the private wrapped builder for a mock. `test_is_supported_returns_false_without_model` covers the same behavior through the real SDK path. * `test_passes_ai_client_event_dispatcher_to_wrapped_builder` is covered by `test_generate_embedding_result_fires_lifecycle_action_hooks`, which fails if the dispatcher is not passed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Av4uVj7v96r2bUKX7kMd8z
Update the bundled PHP AI Client from 1.3.1 to 1.5.0 and introduce `wp_ai_client_embedding()` and `WP_AI_Client_Embedding_Builder` for generating embedding vectors from text and file inputs. Accept multiple inputs as variadic arguments or a single list, and require an explicit model to keep vectors comparable. Extract shared prompt and embedding builder behavior into the internal `WP_AI_Client_Builder` base class, including fluent method proxying, `WP_Error` handling, request timeouts, and clone isolation. Add the `wp_ai_client_prevent_embedding` filter and embedding lifecycle actions, and pass the builder type to the existing timeout filter. Include tests for embedding input handling, model selection, error handling, filters, lifecycle actions, and clone isolation. Developed in: #12530. Props jason_the_adams, gziolo, dkotter, jorgefilipecosta, extrachill, ugyensupport, jeffpaul, jorbin, saadtajik, vedantere, arkaprabhachowdhury, jamesbregenzer. Fixes #65638. git-svn-id: https://develop.svn.wordpress.org/trunk@64327 602fd350-edb4-49c9-b593-d223f7449a82
Update the bundled PHP AI Client from 1.3.1 to 1.5.0 and introduce `wp_ai_client_embedding()` and `WP_AI_Client_Embedding_Builder` for generating embedding vectors from text and file inputs. Accept multiple inputs as variadic arguments or a single list, and require an explicit model to keep vectors comparable. Extract shared prompt and embedding builder behavior into the internal `WP_AI_Client_Builder` base class, including fluent method proxying, `WP_Error` handling, request timeouts, and clone isolation. Add the `wp_ai_client_prevent_embedding` filter and embedding lifecycle actions, and pass the builder type to the existing timeout filter. Include tests for embedding input handling, model selection, error handling, filters, lifecycle actions, and clone isolation. Developed in: WordPress/wordpress-develop#12530. Props jason_the_adams, gziolo, dkotter, jorgefilipecosta, extrachill, ugyensupport, jeffpaul, jorbin, saadtajik, vedantere, arkaprabhachowdhury, jamesbregenzer. Fixes #65638. Built from https://develop.svn.wordpress.org/trunk@64327 git-svn-id: http://core.svn.wordpress.org/trunk@63477 1a063a9b-81f0-0310-95a4-ce76da25c4cd
Trac: https://core.trac.wordpress.org/ticket/65638
What
Updates the bundled PHP AI Client library to 1.4.0 and introduces embedding generation support in core.
Library update
Regenerated
src/wp-includes/php-ai-client/viabash tools/php-ai-client/installer.sh --version=1.4.0. The headline feature of 1.4.0 is embedding generation: a newEmbeddingBuilder, embedding lifecycle events,Embedding/EmbeddingResultDTOs, and a sharedModelResolver. See the 1.4.0 and 1.5.0 release notes for a full list of changes.New:
WP_AI_Client_Embedding_BuilderA WordPress wrapper around the SDK's new
EmbeddingBuilder, following the exact pattern established byWP_AI_Client_Prompt_Builder:__call().WP_Errorhandling instead of exceptions: non-generating methods never throw; once an error occurs the instance enters an error state that preserves the fluent interface, and theWP_Erroris returned from the generating methods (generate_embedding_result(),generate_embedding(),generate_embeddings()).wp_supports_ai()and the existingwp_ai_client_default_request_timeoutfilter.wp_ai_client_prevent_embeddingfilter, analogous towp_ai_client_prevent_prompt.embedding_*prefix (e.g.embedding_network_error,embedding_invalid_argument) with HTTP status codes for REST-friendly errors.Also introduces the variadic
wp_ai_client_embedding()entry point, analogous towp_ai_client_prompt():Testing
php -land PHPCS pass on all changed files.WordPress\AiClient\AiClientresolves after the update.EmbeddingBuildermethods.🤖 Generated with Claude Code
Shared base class:
WP_AI_Client_BuilderSince the prompt and embedding builders share nearly all of their machinery, it now lives in an abstract
WP_AI_Client_Builderbase class: the snake_case__call()proxying,WP_Errorerror-state handling, exception-to-WP_Errorconversion, and default request timeout setup. Child classes supply the SDK builder, error code prefix, prevent filter, and method maps via abstract methods. No behavior changes forWP_AI_Client_Prompt_Builder— error codes, filter names, and_doing_it_wrong()notices (which report the concrete class name) are identical to 7.0.Test updates
PHP AI Client 1.4.0 moved model selection state (
model,registry,providerIdOrClassName,requestOptions) from the SDKPromptBuilderonto its newModelResolver; the test reflection helper now follows properties there.Dedicated tests were added for
WP_AI_Client_Embedding_Builderandwp_ai_client_embedding(), covering the wrapper behavior (not the SDK): constructor input parsing, timeout filter handling, fluent proxying and error-state semantics, thewp_supports_ai()/wp_ai_client_prevent_embeddinggates, exception-to-WP_Errormapping, and generation via a mock embedding model. The fullai-clientgroup passes: 298 tests, 747 assertions.