DEV Community

Illia Kopturov
Illia Kopturov

Posted on

WordPress 7.0 Shipped a Native AI Client. Nobody's Building On It. I Did.

WordPress 7.0 quietly shipped something big: a native AI Client
(wp_ai_client_prompt()), baked straight into WordPress core.

One function call. Any registered provider — Anthropic, OpenAI, Google. No API
key wrangling in your plugin's settings page. No vendor lock-in for your users.

I went looking for who'd built a real product on top of it.

Core team demos. A couple of proof-of-concept gists. That's about it.

So I built one. This post is about the architecture, not a sales pitch — I'll
drop the link at the end for anyone curious, but the point here is the pattern,
because if you build WordPress plugins, you're going to run into this problem
soon whether you touch AI or not.

The problem every AI-powered plugin has been quietly ignoring

If you've shipped or used an "AI content" plugin in the last two years, it
almost certainly works like this:

  • Plugin ships with a settings field for "OpenAI API Key"
  • You paste in a key, it gets stored in wp_options (sometimes encrypted, often not)
  • You're locked to whatever provider that plugin's developer chose to integrate
  • If you use three AI-powered plugins, you're pasting the same key (or three different keys) into three different settings screens, trusting three different developers' key-handling code

That's not a WordPress problem, it's every plugin author solving the same
integration problem badly, in isolation, forever.

wp_ai_client_prompt() fixes this at the platform level. The user connects a
provider once, in core, under Settings → Connectors. Every plugin that
wants AI capability just calls the client — it doesn't know or care which
provider is behind it:

$result = wp_ai_client_prompt()
    ->with_text( 'Write a 150-word product description for a ceramic espresso cup.' )
    ->using_max_tokens( 300 )
    ->generate_text();

if ( ! is_wp_error( $result ) ) {
    echo esc_html( $result );
}
Enter fullscreen mode Exit fullscreen mode

It's a fluent builder: with_text(), using_system_instruction(), using_max_tokens(),
using_model_preference() — chain what you need, call generate_text(), get a string back
or a WP_Error. No provider-specific request shape to build, no provider-specific error
format to catch.

No key in my plugin's database. No provider lock-in for the user. If they
swap their connector from Claude to GPT-4o next month, every plugin using the
AI Client just... uses GPT-4o next month. Zero code change on my end.

This is the same shift wp_mail() was for email transport twenty years ago —
boring, structural, and exactly the kind of thing that matters more than any
single feature built on top of it.

What I built on it: Wordvane

Wordvane is an SEO content generator that lives inside wp-admin. You fill out
a business profile once — what you do, who you sell to, brand voice, up to
three products you want linked — and it generates full, structured articles:
keyword research, secondary keywords, meta title/description, FAQ schema, the
whole publish-ready package, converted straight into native Gutenberg blocks
(not a wall of HTML you have to paste and reformat).

The reason this post exists at all is that none of that required me to touch
a single provider SDK, handle a single API key, or build my own "choose your
AI model" settings screen. The AI Client did that. I got to spend my time on
the actual product: article structure, SEO scoring, internal linking, not
plumbing.

Here are the parts I think are actually worth showing, not just describing.

1. Detecting whether a provider is even connected

Before Wordvane lets anyone generate anything, it checks the AI Client's own
provider registry — not a Wordvane setting, the core registry:

function wordvane_has_configured_ai_provider(): bool {
    if ( ! class_exists( 'WordPress\AiClient\AiClient' ) ) {
        return false;
    }
    $registry = \WordPress\AiClient\AiClient::defaultRegistry();
    foreach ( $registry->getRegisteredProviderIds() as $id ) {
        if ( $registry->isProviderConfigured( $id ) ) {
            return true;
        }
    }
    return false;
}
Enter fullscreen mode Exit fullscreen mode

If nothing's connected, Wordvane doesn't error out mid-generation — it shows an
admin notice pointing the user at Settings → Connectors before they ever
hit "Generate." Small thing, but it's the difference between "plugin is
broken" and "you haven't finished setup yet," and it costs four lines because
the registry is public API, not something I had to build myself.

2. The actual generation call, end to end

This is the real shape, not a simplified stand-in — system instruction, user
message, token budget, optional model preference, all chained onto one client
call:

$prompt = wp_ai_client_prompt()
    ->using_system_instruction( $system_prompt )
    ->with_text( $user_message )
    ->using_max_tokens( $max_tokens );

if ( ! empty( $model_preference ) ) {
    $prompt = $prompt->using_model_preference( $model_preference );
}

$result = $prompt->generate_text();

if ( is_wp_error( $result ) ) {
    wp_send_json_error( [ 'message' => $result->get_error_message() ] );
    return;
}
Enter fullscreen mode Exit fullscreen mode

using_model_preference() is the interesting one — it's a plain string
(e.g. a specific Claude or GPT model name), and the Client resolves it against
whatever's actually connected. If the preferred model isn't available, it
falls back gracefully instead of hard-failing. I never write provider-specific
branching anywhere in this codebase.

3. Getting structured data back without relying on provider-specific JSON modes

Not every provider behind the AI Client supports the same structured-output
or tool-calling features yet, so instead of depending on that, Wordvane makes
a second, smaller generation call against the already-generated article and
just asks for JSON in plain text:

$prompt_text = "Output ONLY a valid JSON object — no explanation, no code "
    . "fences, no markdown.\n\nRequired fields:\n"
    . "- meta_title, meta_description, slug, tags, faq_schema\n\n"
    . "Article:\n" . $plain_text_article;

$response = wp_ai_client_prompt()
    ->with_text( $prompt_text )
    ->using_max_tokens( 1200 )
    ->generate_text();

$json_str = trim( $response );
if ( strncmp( $json_str, '```

', 3 ) === 0 ) {
    $json_str = preg_replace( '/^

```[a-z]*\n?/', '', $json_str );
    $json_str = rtrim( $json_str, " \n`" );
}

$meta = json_decode( $json_str, true );
Enter fullscreen mode Exit fullscreen mode

Unglamorous, but it works identically across every provider today, and it
means I'm not blocked waiting on structured-output support to reach parity
across Anthropic, OpenAI, and Google's implementations.

4. Converting AI output into real Gutenberg blocks, not an HTML blob

The generated article comes back as flat HTML. Pasting that into a post
leaves you with a classic-editor-style content blob sitting inside the block
editor — technically fine, practically a mess to edit afterward. Wordvane
walks the DOM and emits real block comments:

private function node_to_block( DOMDocument $dom, DOMNode $node ) {
    $tag   = strtolower( $node->nodeName );
    $inner = $this->inner_html( $dom, $node );

    switch ( $tag ) {
        case 'h2':
            return "<!-- wp:heading -->\n<h2 class=\"wp-block-heading\">{$inner}</h2>\n<!-- /wp:heading -->\n\n";
        case 'p':
            return "<!-- wp:paragraph -->\n<p>{$inner}</p>\n<!-- /wp:paragraph -->\n\n";
        // ...ul/ol handled the same way, wrapped as wp:list / wp:list-item
    }
}
Enter fullscreen mode Exit fullscreen mode

Every paragraph, heading, and list item lands as an editable native block —
you can rearrange or delete a paragraph the same way you would on any post
you wrote by hand.

5. Regression-testing prompt behavior with deliberately "loud" test data

This is the one I'd actually push other WP+AI plugin developers to steal.
Early on I hit a bug where brand names and product details leaked into
articles that were supposed to be neutral, regardless of what the user had
configured. The fix wasn't a prompt tweak — it was realizing that a business
name embedded in a free-text field (like what_they_sell containing "Acme Co
premium widgets") skips right past a naive "just omit the business_name
field" filter.

The regression tests use a deliberately unmistakable fake brand so any leak
is impossible to miss in a diff or a failing assertion:

$this->dna = [
    'business_name'  => 'GlacierBrew Co',
    'what_they_sell' => 'GlacierBrew Co cold-process ales, brewed in small batches',
    // brand name embedded in a *different* field on purpose
];

public function test_none_system_prompt_excludes_what_they_sell(): void {
    $prompt = Wordvane_Prompt_Builder::build_system_prompt(
        $this->dna, 'craft beer fermentation', '', 'how-to', 'none'
    );

    $this->assertStringNotContainsStringIgnoringCase(
        'GlacierBrew',
        $prompt,
        'Brand name embedded in what_they_sell must not leak into none-mode prompt'
    );
}
Enter fullscreen mode Exit fullscreen mode

If you're building anything that assembles prompts from user-supplied
profile data, this pattern is worth copying directly: pick a fake brand name
nobody would type by accident, plant it in every field that could plausibly
carry it, and assert it's gone from whichever mode is supposed to suppress it.
It turns "I think I fixed the leak" into something CI actually enforces.

Screenshots

Business DNA profile
Business DNA profile — this is the only setup step; every article pulls from it.

Generate screen
Generate screen — provider selection reads directly from whatever's connected under Settings → Connectors.

Gutenberg blocks
Output arrives as real Gutenberg blocks, not raw HTML to clean up.

Live SEO Score panel
Six checks before publish — keyword placement, meta length, word count, FAQ presence.

Try it / build on it

If you're a WordPress plugin developer sitting on an idea that needs AI and
you haven't looked at wp_ai_client_prompt() yet — that's the actual thing
worth taking away from this post. It's a small API surface and it removes an
entire category of problem (key storage, provider lock-in, settings-screen
bloat) that most of us have been solving badly for years.

Wordvane itself is free to install, unlimited generation on the free tier —
wordvane.com if you want to see what I built with it.

Happy to go deeper in the comments — AI Client internals, the block
conversion, the prompt-leak regression tests, whatever's useful. Genuinely
curious what other WP devs are running into building on this.

Top comments (0)