Replace custom AI plumbing with WordPress 7.0’s core AI client, then add feature checks, access control, and REST endpoints the right way.

Before WordPress 7.0, every AI-enabled plugin kept rebuilding the same stack: settings fields for API keys, one-off wp_remote_post() wrappers, and brittle parsing for whichever provider the plugin happened to support. WordPress 7.0 changes that with a core AI client layer, so your plugin can ask for text generation without owning credentials or provider-specific code. If you want a quick place to assemble the PHP examples in this guide, the functions.php snippet builder is a handy way to generate and export them cleanly.
Imagine a plugin that generates post summaries. Pre-7.0, the implementation usually looked something like this:
wp_optionsThat approach worked, but it forced plugin developers to own infrastructure they never really wanted to maintain. The AI feature itself might be 20 lines of business logic. The surrounding plumbing could easily be 200 lines of provider setup, credential handling, retries, and response normalization.
WordPress 7.0 introduces a different split of responsibilities. Your plugin asks core for an AI result. The site owner chooses and configures a provider through Settings > Connectors. Core itself does not bundle OpenAI, Anthropic, Google, Ollama, or any other provider. Providers are separate plugins: the official set includes OpenAI, Anthropic, and Google, while community connectors cover options like OpenRouter, Ollama, and Mistral.
That distinction matters because it’s the main architectural shift:
At the bottom is the PHP AI Client SDK. It’s provider-agnostic and can exist outside WordPress. On top of that sits the WordPress wrapper, which gives you WordPress-native behavior:
snake_case methods like with_text_prompt()WP_Error return patternsIn practice, most plugin developers will interact with the WordPress layer through wp_ai_client_prompt() and WP_AI_Client_Prompt_Builder.
That also means your plugin should stop touching API keys entirely. You don’t need get_option( 'my_plugin_openai_key' ). You don’t need a custom credentials screen. You don’t need provider-specific settings unless your feature truly depends on a vendor-only capability. The Connectors API owns credential storage, and the shared Connectors screen is where site owners wire providers up.
One release-context note: WordPress 7.0 was delayed beyond the original April 9 date because of the Real-Time Collaboration architecture fix. That delay doesn’t change the implementation guidance here. The WP AI Client ships with 7.0 whenever 7.0 lands, and the migration path for plugin authors stays the same.
The first thing to change is mental, not technical: you are no longer “calling OpenAI from a plugin.” You are asking WordPress for a text-generation result, and WordPress routes that request through the configured provider.
Here is the smallest useful example, exactly in the pattern you should build around:
$result = wp_ai_client_prompt()
->with_text_prompt( 'Summarize this post in one sentence: ' . $post_content )
->generate_text_result();
if ( is_wp_error( $result ) ) {
// Handle error — provider may not be configured, or call failed
error_log( $result->get_error_message() );
return '';
}
return $result->get_text();A few practical notes for plugin code:
wp_ai_client_prompt() gives you a prompt builder instance.with_text_prompt() sets the user-facing prompt body.generate_text_result() executes the call through the configured connector.WP_Error or a GenerativeAiResult.get_text() once the call succeeds.That is the core value proposition: one prompt path, one result shape, no provider parser in your plugin. If you want to turn examples like this into reusable boilerplate for testing or local prototypes, the functions.php snippet builder is a straightforward way to package them.
Here’s a slightly more realistic plugin helper you could drop into a service class:
<?php
function my_plugin_generate_excerpt_from_content( string $post_content ): string {
$result = wp_ai_client_prompt()
->with_text_prompt( 'Write a 24-word excerpt for this post: ' . $post_content )
->generate_text_result();
if ( is_wp_error( $result ) ) {
error_log( 'AI excerpt generation failed: ' . $result->get_error_message() );
return '';
}
return trim( $result->get_text() );
}If your old code had branches like “if provider is OpenAI, parse choices[0].message.content; if provider is Anthropic, parse content[0].text,” that entire branch structure is the part you should delete.
This is the production gotcha most plugin developers will hit first: your plugin does not control whether a provider is configured.
A site owner can install your plugin and never connect an AI provider. Or they can deactivate the provider plugin later. Or they can configure one connector for text generation on staging but not production. If your code assumes the provider exists, you’ll ship a feature that fails in a confusing way.
Use feature detection before making the call:
// Check if the site has a provider configured for text generation
if ( ! wp_ai_client_prompt()->is_supported_text_generation() ) {
// Show a notice or disable the feature — don't hard-error
return new WP_Error(
'no_ai_provider',
__( 'No AI provider is configured. Visit Settings > Connectors to add one.', 'my-plugin' )
);
}The right UX pattern here is graceful degradation:
A bad pattern is throwing a fatal-ish admin experience for a feature the site owner hasn’t enabled yet.
For example, this helper wraps both support detection and execution:
<?php
function my_plugin_summarize_text( string $content ) {
if ( ! wp_ai_client_prompt()->is_supported_text_generation() ) {
return new WP_Error(
'no_ai_provider',
__( 'No AI provider is configured. Visit Settings > Connectors to add one.', 'my-plugin' )
);
}
$result = wp_ai_client_prompt()
->with_text_prompt( 'Summarize in one sentence: ' . $content )
->generate_text_result();
if ( is_wp_error( $result ) ) {
return $result;
}
return $result->get_text();
}That pattern keeps missing-provider logic out of your UI layer and makes your feature easier to test.
The next shift is authorization. Many plugins currently guard AI generation only at the UI level: hide a button from non-admins and assume that’s enough. With a shared AI client in core, it’s better to block prompt execution centrally too.
Use wp_ai_client_prevent_prompt to short-circuit prompts before any provider request happens:
add_filter( 'wp_ai_client_prevent_prompt', function( bool $prevent, WP_AI_Client_Prompt_Builder $builder ): bool {
if ( ! current_user_can( 'manage_options' ) ) {
return true; // Block the prompt — no API call made
}
return $prevent;
}, 10, 2 );This matters in a few scenarios:
You do not need to limit this filter to admins in every plugin, but you should decide intentionally. Common options are:
manage_options for admin-only toolsedit_posts for editorial assistantsThe key point is that access control now belongs close to the prompt boundary, not just the button that happens to trigger it.
For Gutenberg panels, custom admin interfaces, and decoupled front ends, the cleanest integration pattern is a REST endpoint that delegates to wp_ai_client_prompt() internally.
Here is the exact wiring pattern to start with:
add_action( 'rest_api_init', function() {
register_rest_route( 'my-plugin/v1', '/summarize', [
'methods' => 'POST',
'callback' => 'my_plugin_ai_summarize',
'permission_callback' => function() {
return current_user_can( 'edit_posts' );
},
] );
} );
function my_plugin_ai_summarize( WP_REST_Request $request ) {
$prompt = sanitize_text_field( $request->get_param( 'content' ) );
$result = wp_ai_client_prompt()
->with_text_prompt( 'Summarize in one sentence: ' . $prompt )
->generate_text_result();
// rest_ensure_response() handles both WP_Error and GenerativeAiResult
return rest_ensure_response( $result );
}Two details are easy to miss here:
permission_callback should reflect who is allowed to use the feature, even if you also apply the wp_ai_client_prevent_prompt filter.rest_ensure_response() can work directly with both WP_Error and GenerativeAiResult, which keeps the callback compact.For production use, combine REST with feature detection so you return a predictable error when no provider is configured:
<?php
add_action( 'rest_api_init', function() {
register_rest_route( 'my-plugin/v1', '/headline', [
'methods' => 'POST',
'callback' => 'my_plugin_ai_headline',
'permission_callback' => function() {
return current_user_can( 'edit_posts' );
},
'args' => [
'content' => [
'required' => true,
'sanitize_callback' => 'sanitize_textarea_field',
'type' => 'string',
],
],
] );
} );
function my_plugin_ai_headline( WP_REST_Request $request ) {
if ( ! wp_ai_client_prompt()->is_supported_text_generation() ) {
return rest_ensure_response(
new WP_Error(
'no_ai_provider',
__( 'No AI provider is configured. Visit Settings > Connectors to add one.', 'my-plugin' ),
[ 'status' => 400 ]
)
);
}
$content = $request->get_param( 'content' );
$result = wp_ai_client_prompt()
->with_text_prompt( 'Write a short SEO-friendly headline for: ' . $content )
->generate_text_result();
return rest_ensure_response( $result );
}This is the pattern to use when a block sidebar says “Generate summary,” when a React settings screen needs suggestions, or when a headless front end posts content to WordPress for enrichment.
The core AI client removes a lot of duplicated plumbing, but it also changes where control lives.
First, your plugin gives up direct ownership of provider configuration. That’s mostly a win, but it means support requests can shift from “your plugin is broken” to “the site has no configured connector” or “the selected provider plugin was deactivated.” If your feature is optional, that’s fine. If your plugin’s entire value depends on AI, you need clear admin notices and capability-aware onboarding.
Second, provider abstraction simplifies common tasks but can hide vendor-specific features. If your old integration used a niche capability that only one provider exposes, you may need to decide whether that feature belongs in a generic core-client flow at all. The sweet spot for wordpress ai integration in plugins is shared text-generation behavior, not every exotic provider knob.
Third, migration is less about rewriting feature logic and more about deleting code responsibly. A solid checklist looks like this:
get_option( 'my_plugin_openai_key' )cURL or wp_remote_post() calls with wp_ai_client_prompt()is_supported_text_generation() checks anywhere AI is optionalwp_ai_client_prevent_prompt when neededHere’s a simplified before-and-after to make the migration concrete.
Before:
<?php
function my_plugin_old_ai_call( string $content ) {
$api_key = get_option( 'my_plugin_openai_key', '' );
if ( '' === $api_key ) {
return new WP_Error( 'missing_key', 'OpenAI API key is missing.' );
}
$response = wp_remote_post( 'https://api.openai.com/v1/chat/completions', [
'headers' => [
'Authorization' => 'Bearer ' . $api_key,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode( [
'model' => 'gpt-4o-mini',
'messages' => [
[ 'role' => 'user', 'content' => 'Summarize: ' . $content ],
],
] ),
] );
if ( is_wp_error( $response ) ) {
return $response;
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
return $data['choices'][0]['message']['content'] ?? '';
}After:
<?php
function my_plugin_new_ai_call( string $content ) {
if ( ! wp_ai_client_prompt()->is_supported_text_generation() ) {
return new WP_Error(
'no_ai_provider',
__( 'No AI provider is configured. Visit Settings > Connectors to add one.', 'my-plugin' )
);
}
$result = wp_ai_client_prompt()
->with_text_prompt( 'Summarize: ' . $content )
->generate_text_result();
if ( is_wp_error( $result ) ) {
return $result;
}
return $result->get_text();
}That’s the migration in one glance: no key management, no vendor endpoint, no vendor response structure, same plugin outcome.
WordPress 7.0’s AI client changes plugin development in exactly the place that was most repetitive: the plumbing. Instead of shipping your own mini AI platform inside every plugin, you can focus on prompts, permissions, and product behavior while core and the Connectors stack handle the provider layer. If you’re updating an existing plugin or prototyping a new one, start by rebuilding one feature with the functions.php snippet builder, then strip out the old credential and HTTP code once the new path is working.