{"id":6094,"date":"2026-05-14T14:54:00","date_gmt":"2026-05-14T14:54:00","guid":{"rendered":"https:\/\/developer.wordpress.org\/news\/?p=6094"},"modified":"2026-05-11T13:41:05","modified_gmt":"2026-05-11T13:41:05","slug":"how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client","status":"publish","type":"post","link":"https:\/\/developer.wordpress.org\/news\/2026\/05\/how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client\/","title":{"rendered":"How to build an image generation plugin with the WordPress AI Client"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/make.wordpress.org\/core\/2026\/03\/24\/introducing-the-ai-client-in-wordpress-7-0\/\">WordPress 7.0 ships a built-in AI Client<\/a>: a PHP API that lets plugins send prompts to AI providers and receive their results. What better way to understand how this new API works than by building something real with it? In this article, you will learn how to use the AI Client by building a plugin that generates images directly in the Media Library.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The plugin is intentionally small. It can send a text prompt to an AI provider, receive a generated image, and you can then save it to the WordPress Media Library if you like. Along the way, you will see the patterns that make AI-powered features work reliably in WordPress \u2014 all using the new built-in AI Client.<\/p>\n\n\n\n<div class=\"wp-block-group has-light-grey-2-background-color has-background is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:2px;margin-top:var(--wp--preset--spacing--30);margin-bottom:var(--wp--preset--spacing--30);padding-top:var(--wp--preset--spacing--30);padding-right:var(--wp--preset--spacing--30);padding-bottom:var(--wp--preset--spacing--30);padding-left:var(--wp--preset--spacing--30)\">\n<p class=\"has-large-font-size wp-block-paragraph\" style=\"font-style:normal;font-weight:600;line-height:1\">Table of Contents<\/p>\n\n\n\n<nav aria-label=\"Table of Contents\" class=\"wp-block-table-of-contents\"><ol><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/developer.wordpress.org\/news\/2026\/05\/how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client\/#how-the-ai-client-works\">How the AI client works<\/a><ol><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/developer.wordpress.org\/news\/2026\/05\/how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client\/#provider-and-model-agnostic\">Provider and model-agnostic<\/a><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/developer.wordpress.org\/news\/2026\/05\/how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client\/#the-entry-point-wp-ai-client-prompt\">The entry point: wp_ai_client_prompt()<\/a><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/developer.wordpress.org\/news\/2026\/05\/how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client\/#model-preferences-not-requirements\">Model preferences, not requirements<\/a><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/developer.wordpress.org\/news\/2026\/05\/how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client\/#support-checks-gate-your-ai-features\">Support checks: gate your AI features<\/a><\/li><\/ol><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/developer.wordpress.org\/news\/2026\/05\/how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client\/#implementing-the-plugin\">Implementing the plugin<\/a><ol><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/developer.wordpress.org\/news\/2026\/05\/how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client\/#prerequisites\">Prerequisites<\/a><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/developer.wordpress.org\/news\/2026\/05\/how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client\/#plugin-file-structure\">Plugin file structure<\/a><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/developer.wordpress.org\/news\/2026\/05\/how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client\/#the-main-plugin-file\">The main plugin file<\/a><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/developer.wordpress.org\/news\/2026\/05\/how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client\/#building-the-prompt\">Building the prompt<\/a><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/developer.wordpress.org\/news\/2026\/05\/how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client\/#the-rest-api\">The REST API<\/a><ol><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/developer.wordpress.org\/news\/2026\/05\/how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client\/#generating-an-image\">Generating an image<\/a><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/developer.wordpress.org\/news\/2026\/05\/how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client\/#uploading-to-the-media-library\">Uploading to the Media Library<\/a><\/li><\/ol><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/developer.wordpress.org\/news\/2026\/05\/how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client\/#admin-integration\">Admin integration<\/a><ol><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/developer.wordpress.org\/news\/2026\/05\/how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client\/#registering-the-script\">Registering the script<\/a><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/developer.wordpress.org\/news\/2026\/05\/how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client\/#conditional-enqueuing-with-support-check\">Conditional enqueuing with support check<\/a><\/li><\/ol><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/developer.wordpress.org\/news\/2026\/05\/how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client\/#the-frontend\">The frontend<\/a><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/developer.wordpress.org\/news\/2026\/05\/how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client\/#build-and-test\">Build and test<\/a><\/li><\/ol><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/developer.wordpress.org\/news\/2026\/05\/how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client\/#wrapping-up\">Wrapping up<\/a><ol><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/developer.wordpress.org\/news\/2026\/05\/how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client\/#looking-back-the-ai-integration-is-small\">Looking back, the AI integration is small<\/a><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/developer.wordpress.org\/news\/2026\/05\/how-to-build-an-image-generation-plugin-with-the-wordpress-ai-client\/#going-further\">Going further<\/a><\/li><\/ol><\/li><\/ol><\/nav>\n<\/div>\n\n\n\n<h2 id=\"how-the-ai-client-works\" class=\"wp-block-heading\">How the AI client works<\/h2>\n\n\n\n<h3 id=\"provider-and-model-agnostic\" class=\"wp-block-heading\">Provider and model-agnostic<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The most important concept behind the WordPress AI Client is that your plugin never talks to an AI provider directly. You describe <em>what<\/em> you need \u2014 text, an image, a specific output format \u2014 and <em>how<\/em> you need it \u2014 aspect ratio for images, temperature for text generation, and so on. With that information, WordPress routes the request to a suitable model from a provider the site owner has configured.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is relevant for several reasons:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Site owners stay in control.<\/strong> They choose their provider (Anthropic, Google, OpenAI, or any other supported service) and manage API keys through <a href=\"https:\/\/make.wordpress.org\/core\/2026\/03\/18\/introducing-the-connectors-api-in-wordpress-7-0\/\">the <strong>Settings &gt; Connectors<\/strong> screen<\/a> \u2014 a centralized admin interface for managing service connections. Your plugin does not need its own settings page for API credentials.<\/li>\n\n\n\n<li><strong>Plugins are portable.<\/strong> A plugin that works with one provider works with all of them. There are no provider-specific code paths to maintain.<\/li>\n\n\n\n<li><strong>The ecosystem scales.<\/strong> As new providers and models become available, existing plugins gain support automatically \u2014 no plugin updates required.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">This means you should never write code that assumes a particular provider or model. Instead, you describe your requirements, and the AI Client finds a suitable match.<\/p>\n\n\n\n<h3 id=\"the-entry-point-wp-ai-client-prompt\" class=\"wp-block-heading\">The entry point: wp_ai_client_prompt()<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Every interaction with the AI Client starts with a single function call:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"php\" class=\"language-php\">$builder = wp_ai_client_prompt();<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This returns a <a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/blob\/trunk\/src\/wp-includes\/ai-client\/class-wp-ai-client-prompt-builder.php\">WP_AI_Client_Prompt_Builder<\/a> instance \u2014 a fluent builder that follows WordPress naming conventions (snake_case methods) while wrapping the underlying <a href=\"https:\/\/github.com\/WordPress\/php-ai-client\">php-ai-client<\/a> library.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">From there, you chain methods to describe your prompt. For example, to generate text:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"php\" class=\"language-php\">$builder = wp_ai_client_prompt()\n&nbsp;&nbsp;&nbsp;&nbsp;-&gt;with_text( 'Summarize the benefits of caching in WordPress' )\n&nbsp;&nbsp;&nbsp;&nbsp;-&gt;using_temperature( 0.7 );<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>with_text()<\/code> provides the prompt. <code>using_temperature()<\/code> is an optional configuration parameter that controls randomness in the output. When you are ready, call a generation method:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"php\" class=\"language-php\">$result = $builder-&gt;generate_text_result();<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>$result<\/code> is a <a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/blob\/a46327116b184ee43777c9d119c8e8ce4cae0ede\/src\/wp-includes\/php-ai-client\/src\/Results\/DTO\/GenerativeAiResult.php\"><code>GenerativeAiResult<\/code><\/a> object \u2014 a serializable data transfer object that contains the generated content, along with metadata about which provider and model handled the request. You can pass it directly to <a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/rest_ensure_response\/\"><code>rest_ensure_response()<\/code><\/a>, which makes it straightforward to expose AI features through the REST API.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The same builder pattern works for image generation, which is the focus of this article. You will see how to configure a builder for images \u2014 with output file type, aspect ratio, and more \u2014 in the sections below.<\/p>\n\n\n\n<h3 id=\"model-preferences-not-requirements\" class=\"wp-block-heading\">Model preferences, not requirements<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Since your plugin does not control which provider or model is available on each individual WordPress site, the AI Client uses a preference system rather than hard requirements.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You can call <code>using_model_preference()<\/code> with one or more model slugs to indicate which models would be ideal for your use case. The AI Client tries them in order \u2014 if the first is available, it uses that; otherwise it falls back to the next, and so on. If none of the preferred models are available, it falls back to any model that supports the requested capability. Your plugin continues to work either way.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"php\" class=\"language-php\">$builder = wp_ai_client_prompt()\n&nbsp;&nbsp;&nbsp;&nbsp;-&gt;with_text( 'Summarize the benefits of caching in WordPress' )\n&nbsp;&nbsp;&nbsp;&nbsp;-&gt;using_temperature( 0.7 )\n&nbsp;&nbsp;&nbsp;&nbsp;-&gt;using_model_preference( 'gpt-5.4', 'gemini-3.1-pro-preview', 'claude-opus-4.6' );<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is a preference, not a requirement. Building around a specific model would force vendor lock-in upon your plugin&#8217;s users or prevent them from using your plugin \u2014 it would defeat the purpose of the abstraction. Use model preferences when a particular model handles your use case especially well, but design your plugin to function without them.<\/p>\n\n\n\n<h3 id=\"support-checks-gate-your-ai-features\" class=\"wp-block-heading\">Support checks: gate your AI features<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Not every WordPress site will have an AI provider configured, and not every provider supports every capability or configuration option. Before exposing an AI feature to users, check whether it can actually work. For example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"php\" class=\"language-php\">$is_available = wp_ai_client_prompt()\n    -&gt;with_text( 'test' )\n&nbsp;&nbsp;&nbsp;&nbsp;-&gt;is_supported_for_image_generation();<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>is_supported_for_image_generation()<\/code> returns true only when the site has a configured provider with a model that supports image generation. There are equivalent methods for other capabilities like text generation.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This check does not make an API call or perform any inference. The prompt text itself is not analyzed for the support check. The method uses purely deterministic logic, matching the builder&#8217;s configuration (requested capability, output options, etc.) against the capabilities the available models declare. There is no cost incurred by calling it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This check is essential. Use it to conditionally load your UI, show a helpful notice when the feature is unavailable, or skip registering UI items entirely. Never assume that AI features will be available just because WordPress 7.0 is installed \u2014 the site owner still needs to set up a provider.<\/p>\n\n\n\n<h2 id=\"implementing-the-plugin\" class=\"wp-block-heading\">Implementing the plugin<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The plugin adds a &#8220;Generate Image File&#8221; button to the Media Library. Clicking it opens a dialog where the user enters a text prompt. The plugin sends the prompt to the AI Client, displays the generated image as a preview, and lets the user save it as a new media attachment.<\/p>\n\n\n\n<figure data-wp-context=\"{&quot;imageId&quot;:&quot;6a1023ea39a14&quot;}\" data-wp-interactive=\"core\/image\" data-wp-key=\"6a1023ea39a14\" class=\"wp-block-image size-full wp-lightbox-container\"><img loading=\"lazy\" decoding=\"async\" width=\"2472\" height=\"1618\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on--pointerdown=\"actions.preloadImage\" data-wp-on--pointerenter=\"actions.preloadImageWithDelay\" data-wp-on--pointerleave=\"actions.cancelPreload\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/developer.wordpress.org\/news\/files\/2026\/05\/wp-ai-client-imagegen-ui.webp\" alt=\"Screenshot of the WordPress Media Library with a &quot;Generate Image&quot; modal open, showing a prompt text input field with a &quot;Generate Image&quot; button, the generated image of a Cavalier King Charles Spaniel, and a separate UI for uploading the image to the Media Library\" class=\"wp-image-6099\" srcset=\"https:\/\/developer.wordpress.org\/news\/files\/2026\/05\/wp-ai-client-imagegen-ui.webp 2472w, https:\/\/developer.wordpress.org\/news\/files\/2026\/05\/wp-ai-client-imagegen-ui-300x196.webp 300w, https:\/\/developer.wordpress.org\/news\/files\/2026\/05\/wp-ai-client-imagegen-ui-767x502.webp 767w, https:\/\/developer.wordpress.org\/news\/files\/2026\/05\/wp-ai-client-imagegen-ui-1024x670.webp 1024w, https:\/\/developer.wordpress.org\/news\/files\/2026\/05\/wp-ai-client-imagegen-ui-1536x1005.webp 1536w, https:\/\/developer.wordpress.org\/news\/files\/2026\/05\/wp-ai-client-imagegen-ui-2048x1340.webp 2048w\" sizes=\"auto, (max-width: 2472px) 100vw, 2472px\" \/><button\n\t\t\tclass=\"lightbox-trigger\"\n\t\t\ttype=\"button\"\n\t\t\taria-haspopup=\"dialog\"\n\t\t\tdata-wp-bind--aria-label=\"state.thisImage.triggerButtonAriaLabel\"\n\t\t\tdata-wp-init=\"callbacks.initTriggerButton\"\n\t\t\tdata-wp-on--click=\"actions.showLightbox\"\n\t\t\tdata-wp-style--right=\"state.thisImage.buttonRight\"\n\t\t\tdata-wp-style--top=\"state.thisImage.buttonTop\"\n\t\t>\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewBox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\" \/>\n\t\t\t<\/svg>\n\t\t<\/button><figcaption class=\"wp-element-caption\">Screenshot of the image generation modal added by the plugin from this article<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The full source code is available on GitHub at <a href=\"https:\/\/github.com\/wptrainingteam\/ai-client-imagegen\">wptrainingteam\/ai-client-imagegen<\/a>. <\/p>\n\n\n\n<div class=\"wp-block-wporg-notice is-info-notice\"><div class=\"wp-block-wporg-notice__icon\"><\/div><div class=\"wp-block-wporg-notice__content\"><p>Note: Some code snippets in this article were slightly reduced from the code in the plugin for better focus on the most relevant changes.<\/p><\/div><\/div>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h3 id=\"prerequisites\" class=\"wp-block-heading\">Prerequisites<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">To follow along, you will need:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>WordPress 7.0<\/strong> (or later) installed and running.<\/li>\n\n\n\n<li><strong>An AI provider configured<\/strong> with image generation support. Go to <strong>Settings &gt; Connectors<\/strong> in the WordPress admin and add a connection for a service like OpenAI or Google AI. Make sure the configured provider offers one or more models that can generate images.<\/li>\n\n\n\n<li><strong>Node.js<\/strong> (version 20 or later) for building the frontend assets.<\/li>\n\n\n\n<li>A basic understanding of WordPress plugin development, the REST API, and PHP.<\/li>\n<\/ul>\n\n\n\n<h3 id=\"plugin-file-structure\" class=\"wp-block-heading\">Plugin file structure<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The plugin uses a straightforward structure:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">ai-client-imagegen\/\n\u251c\u2500\u2500 plugin.php\n\u251c\u2500\u2500 includes\/\n\u2502 &nbsp; \u251c\u2500\u2500 prompt.php\n\u2502 &nbsp; \u251c\u2500\u2500 rest-api.php\n\u2502 &nbsp; \u2514\u2500\u2500 admin.php\n\u251c\u2500\u2500 src\/\n\u2502 &nbsp; \u2514\u2500\u2500 index.ts\n\u251c\u2500\u2500 build\/\n\u2502 &nbsp; \u251c\u2500\u2500 index.js\n\u2502 &nbsp; \u2514\u2500\u2500 index.asset.php\n\u251c\u2500\u2500 package.json\n\u2514\u2500\u2500 composer.json<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>plugin.php<\/code> \u2014 Main entry point. Loads everything, registers hooks.<\/li>\n\n\n\n<li><code>includes\/prompt.php<\/code> \u2014 Reusable functions that build AI Client prompts.<\/li>\n\n\n\n<li><code>includes\/rest-api.php<\/code> \u2014 REST API endpoint for generating images and saving them to the Media Library.<\/li>\n\n\n\n<li><code>includes\/admin.php<\/code> \u2014 Script registration and enqueuing on the Media Library screen.<\/li>\n\n\n\n<li><code>src\/index.ts<\/code> \u2014 Frontend: the dialog UI and API calls.<\/li>\n\n\n\n<li><code>build\/<\/code> \u2014 Compiled JavaScript output (generated by wordpress\/scripts).<\/li>\n<\/ul>\n\n\n\n<h3 id=\"the-main-plugin-file\" class=\"wp-block-heading\">The main plugin file<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>plugin.php<\/code> file is minimal. It declares the plugin header, gates on the AI Client being available, loads the include files, and registers hooks:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"php\" class=\"language-php\">&lt;?php\n\/**\n&nbsp;* Plugin Name: AI Client ImageGen\n&nbsp;* Plugin URI: https:\/\/github.com\/wptrainingteam\/ai-client-imagegen\n&nbsp;* Description: Generates images in the WordPress Media Library using the built-in AI Client.\n&nbsp;* Requires at least: 7.0\n&nbsp;* Requires PHP: 7.4\n&nbsp;* Version: 1.0.0\n&nbsp;* Author: Your Name\n&nbsp;* License: GPL-2.0-or-later\n&nbsp;* Text Domain: ai-client-imagegen\n&nbsp;*\/\n\nif ( ! defined( 'ABSPATH' ) ) {\n&nbsp;&nbsp;&nbsp;&nbsp;exit;\n}\n\nif ( ! function_exists( 'wp_ai_client_prompt' ) ) {\n&nbsp;&nbsp;&nbsp;&nbsp;return;\n}\n\nrequire_once __DIR__ . '\/includes\/prompt.php';\nrequire_once __DIR__ . '\/includes\/rest-api.php';\nrequire_once __DIR__ . '\/includes\/admin.php';\n\nadd_action( 'rest_api_init', 'aicig_register_rest_routes' );\nadd_action( 'init', 'aicig_register_assets' );\nadd_action( 'admin_enqueue_scripts', 'aicig_enqueue_media_assets' );<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Setting <code>Requires at least: 7.0<\/code> in the plugin header ensures WordPress will not activate the plugin on older versions. The <code>function_exists( 'wp_ai_client_prompt' )<\/code> check serves as a runtime safety net \u2014 if the AI Client function is not available for any reason, the plugin bails out early.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The three <code>add_action()<\/code>calls wire up the REST API routes, asset registration, and conditional script enqueuing. Each callback is defined in its own file.<\/p>\n\n\n\n<h3 id=\"building-the-prompt\" class=\"wp-block-heading\">Building the prompt<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This is the core of the AI Client integration. The <code>includes\/prompt.php<\/code> file defines a reusable function that configures a prompt builder for image generation:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"php\" class=\"language-php\">&lt;?php\nuse WordPress\\AiClient\\Files\\Enums\\FileTypeEnum;\nuse WordPress\\AiClient\\Files\\Enums\\MediaOrientationEnum;\n\nfunction aicig_get_image_generation_prompt( string $prompt, string $orientation = '' ): WP_AI_Client_Prompt_Builder {\n&nbsp;&nbsp;&nbsp;&nbsp;$builder = wp_ai_client_prompt()\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;with_text( $prompt )\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;as_output_file_type( FileTypeEnum::inline() );\n\n&nbsp;&nbsp;&nbsp;&nbsp;if ( $orientation ) {\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$builder-&gt;as_output_media_orientation( MediaOrientationEnum::from( $orientation ) );\n&nbsp;&nbsp;&nbsp;&nbsp;}\n\n&nbsp;&nbsp;&nbsp;&nbsp;return $builder;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A few things to note:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>The function returns the builder, not the result.<\/strong> This is a deliberate design choice. The caller decides when to execute the prompt (via <code>generate_image_result()<\/code>) and can add further configuration. It also makes the builder available for support checks \u2014 the same function is reused to call <code>is_supported_for_image_generation()<\/code>, as you will see later in the admin integration.<\/li>\n\n\n\n<li><strong><code>FileTypeEnum::inline()<\/code><\/strong> requests base64-encoded image data in the response. This is convenient for a plugin that needs to display the image in the browser before saving it.<\/li>\n\n\n\n<li><strong><code>MediaOrientationEnum<\/code><\/strong> is optional. If provided, it hints at the desired aspect ratio \u2014 <code>square<\/code>, <code>landscape<\/code>, or <code>portrait<\/code>. The method <code>MediaOrientationEnum::from()<\/code> converts the string to the enum value.<\/li>\n\n\n\n<li><strong>No provider or model is specified.<\/strong> The AI Client handles routing. The prompt describes <em>what<\/em> is needed; WordPress determines <em>how<\/em> to fulfill it. This function will work with any provider that supports image generation.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Wrapping the prompt configuration in a standalone function keeps it reusable. The REST API calls it to generate images, and the admin code calls it to check whether image generation is supported.<\/p>\n\n\n\n<h3 id=\"the-rest-api\" class=\"wp-block-heading\">The REST API<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The plugin exposes two REST endpoints under the <code>ai-client-imagegen\/v1<\/code> namespace: one for generating images and one for uploading a generated image to the Media Library. Alternatively, a single endpoint could generate the image and save it to the Media Library in one step. However, since users should ideally review generated images before committing to them, splitting the two concerns makes more sense.<\/p>\n\n\n\n<h4 id=\"generating-an-image\" class=\"wp-block-heading\">Generating an image<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">The generation route takes a text <code>prompt<\/code> and an optional <code>orientation<\/code>. Its callback, which can be defined in the <code>includes\/rest-api.php<\/code> file, uses the reusable prompt function from <code>includes\/prompt.php<\/code>, calls <code>generate_image_result()<\/code>, and returns the result directly:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"php\" class=\"language-php\">function aicig_register_rest_routes(): void {\n    register_rest_route(\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'ai-client-imagegen\/v1',\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'\/generate-image',\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;array(\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'methods' &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; =&gt; WP_REST_Server::CREATABLE,\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'callback'&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; =&gt; 'aicig_rest_generate_image',\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'permission_callback' =&gt; static function () {\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return current_user_can( 'upload_files' );\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;},\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'args'&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; =&gt; array(\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'prompt'&nbsp; &nbsp; &nbsp; =&gt; array(\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'type' &nbsp; &nbsp; =&gt; 'string',\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'required' =&gt; true,\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;),\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'orientation' =&gt; array(\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'type' &nbsp; &nbsp; =&gt; 'string',\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'required' =&gt; false,\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'enum' &nbsp; &nbsp; =&gt; array( 'square', 'landscape', 'portrait' ),\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;),\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;),\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;)\n&nbsp;&nbsp;&nbsp;&nbsp;);\n}\n\nfunction aicig_rest_generate_image( WP_REST_Request $request ) {\n&nbsp;&nbsp;&nbsp;&nbsp;$prompt&nbsp; &nbsp; &nbsp; = $request-&gt;get_param( 'prompt' );\n&nbsp;&nbsp;&nbsp;&nbsp;$orientation = $request-&gt;get_param( 'orientation' );\n\n&nbsp;&nbsp;&nbsp;&nbsp;$builder = aicig_get_image_generation_prompt(\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$prompt,\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$orientation ?? ''\n&nbsp;&nbsp;&nbsp;&nbsp;);\n\n&nbsp;&nbsp;&nbsp;&nbsp;return rest_ensure_response(\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$builder-&gt;generate_image_result()\n&nbsp;&nbsp;&nbsp;&nbsp;);\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Because the <code>GenerativeAiResult<\/code> object that <code>generate_image_result()<\/code> returns is serializable, <code>rest_ensure_response()<\/code> converts it directly into a JSON response. The response includes the generated image data, plus metadata about which provider and model handled the request \u2014 information the frontend can display as attribution.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Similarly, if an error occurs as part of this, <code>generate_image_result()<\/code> returns a <code>WP_Error<\/code>, which can also be processed as a REST response directly \u2014 no conditional handling necessary.<\/p>\n\n\n\n<h4 id=\"uploading-to-the-media-library\" class=\"wp-block-heading\">Uploading to the Media Library<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">Once the user is happy with the generated image, the frontend sends it to a second endpoint. This route takes the base64-encoded image data, a file name, and a MIME type. The callback decodes the data, writes it to disk, and creates a WordPress attachment:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"php\" class=\"language-php\">function aicig_register_rest_routes(): void {\n    \/\/ Other REST route registration for generating an image.\n    register_rest_route(\n        'ai-client-imagegen\/v1',\n        '\/upload-image',\n        array(\n            'methods' &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; =&gt; WP_REST_Server::CREATABLE,\n            'callback'&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; =&gt; 'aicig_rest_upload_image',\n            'permission_callback' =&gt; static function () {\n                return current_user_can( 'upload_files' );\n            },\n            'args'&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; =&gt; array(\n                'image_base64' =&gt; array(\n                    'type' &nbsp; &nbsp; =&gt; 'string',\n                    'required' =&gt; true,\n                ),\n                'file_name'&nbsp; &nbsp; =&gt; array(\n                    'type'&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; =&gt; 'string',\n                    'required'&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; =&gt; true,\n                    'sanitize_callback' =&gt; 'sanitize_file_name',\n                ),\n                'mime_type'&nbsp; &nbsp; =&gt; array(\n                    'type'&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; =&gt; 'string',\n                    'required'&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; =&gt; false,\n                    'default' &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; =&gt; 'image\/png',\n                    'sanitize_callback' =&gt; 'sanitize_mime_type',\n                ),\n\n            ),\n        )\n    );\n}\n\nfunction aicig_rest_upload_image( WP_REST_Request $request ) {\n&nbsp;&nbsp;&nbsp;&nbsp;$image_base64 = $request-&gt;get_param( 'image_base64' );\n&nbsp;&nbsp;&nbsp;&nbsp;$file_name&nbsp; &nbsp; = $request-&gt;get_param( 'file_name' );\n&nbsp;&nbsp;&nbsp;&nbsp;$mime_type&nbsp; &nbsp; = $request-&gt;get_param( 'mime_type' );\n\n&nbsp;&nbsp;&nbsp;&nbsp;$decoded = base64_decode( $image_base64, true );\n&nbsp;&nbsp;&nbsp;&nbsp;if ( false === $decoded ) {\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return new WP_Error(\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'invalid_image_data',\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;__( 'The provided image data is not valid base64.', 'ai-client-imagegen' ),\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;array( 'status' =&gt; 400 )\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;);\n&nbsp;&nbsp;&nbsp;&nbsp;}\n\n&nbsp;&nbsp;&nbsp;&nbsp;$upload = wp_upload_bits( $file_name, null, $decoded );\n&nbsp;&nbsp;&nbsp;&nbsp;if ( ! empty( $upload['error'] ) ) {\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return new WP_Error(\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'upload_failed',\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$upload['error'],\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;array( 'status' =&gt; 500 )\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;);\n&nbsp;&nbsp;&nbsp;&nbsp;}\n\n&nbsp;&nbsp;&nbsp;&nbsp;$attachment_data = array(\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'post_mime_type' =&gt; $mime_type,\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'post_title' &nbsp; &nbsp; =&gt; sanitize_file_name(\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pathinfo( $file_name, PATHINFO_FILENAME )\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;),\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'post_status'&nbsp; &nbsp; =&gt; 'inherit',\n&nbsp;&nbsp;&nbsp;&nbsp;);\n\n&nbsp;&nbsp;&nbsp;&nbsp;$attachment_id = wp_insert_attachment(\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$attachment_data,\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$upload['file']\n&nbsp;&nbsp;&nbsp;&nbsp;);\n\n&nbsp;&nbsp;&nbsp;&nbsp;if ( is_wp_error( $attachment_id ) ) {\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return $attachment_id;\n&nbsp;&nbsp;&nbsp;&nbsp;}\n\n&nbsp;&nbsp;&nbsp;&nbsp;require_once ABSPATH . 'wp-admin\/includes\/image.php';\n\n&nbsp;&nbsp;&nbsp;&nbsp;$metadata = wp_generate_attachment_metadata(\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$attachment_id,\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$upload['file']\n&nbsp;&nbsp;&nbsp;&nbsp;);\n\n&nbsp;&nbsp;&nbsp;&nbsp;wp_update_attachment_metadata( $attachment_id, $metadata );\n\n&nbsp;&nbsp;&nbsp;&nbsp;return rest_ensure_response(\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;array(\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'id'&nbsp; =&gt; $attachment_id,\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'url' =&gt; wp_get_attachment_url( $attachment_id ),\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;)\n&nbsp;&nbsp;&nbsp;&nbsp;);\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is standard WordPress media handling \u2014 <code>wp_upload_bits()<\/code> to write the file, <code>wp_insert_attachment()<\/code> to create the database record, and <code>wp_generate_attachment_metadata()<\/code> to create thumbnails and image metadata. Nothing here is specific to AI.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">With this, we&#8217;re done with the server-side foundation for the feature. Let&#8217;s focus on frontend and UI next.<\/p>\n\n\n\n<h3 id=\"admin-integration\" class=\"wp-block-heading\">Admin integration<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The only thing we need PHP for in relation to the frontend is for managing the script that renders the dynamic UI and handles interactions. The <code>includes\/admin.php<\/code> file handles three things: registering the script, conditionally enqueuing it on the Media Library screen, and showing a notice when image generation is not supported.<\/p>\n\n\n\n<h4 id=\"registering-the-script\" class=\"wp-block-heading\">Registering the script<\/h4>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"php\" class=\"language-php\">function aicig_register_assets(): void {\n    $asset_file = plugin_dir_path( __DIR__ ) . 'build\/index.asset.php';\n\n&nbsp;&nbsp;&nbsp;&nbsp;if ( file_exists( $asset_file ) ) {\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$asset = require $asset_file;\n&nbsp;&nbsp;&nbsp;&nbsp;} else {\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$asset = array(\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'dependencies' =&gt; array(),\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'version'&nbsp; &nbsp; &nbsp; =&gt; '1.0.0',\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;);\n&nbsp;&nbsp;&nbsp;&nbsp;}\n\n&nbsp;&nbsp;&nbsp;&nbsp;wp_register_script(\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'aicig-imagegen',\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;plugins_url( 'build\/index.js', __DIR__ ),\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$asset['dependencies'],\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$asset['version'],\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;array( 'strategy' =&gt; 'defer' )\n&nbsp;&nbsp;&nbsp;&nbsp;);\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>build\/index.asset.php<\/code> file is auto-generated by <code>@wordpress\/scripts<\/code> during the build step. It declares the script&#8217;s dependencies and a content-based version hash. This is the <a href=\"https:\/\/developer.wordpress.org\/block-editor\/getting-started\/devenv\/get-started-with-wp-scripts\/#enqueuing-assets\">standard WordPress pattern for bundled scripts<\/a>.<\/p>\n\n\n\n<h4 id=\"conditional-enqueuing-with-support-check\" class=\"wp-block-heading\">Conditional enqueuing with support check<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">In order for the script to actually load in the frontend, we&#8217;ll also need to enqueue it. This is where we need to apply support check first though, so that the script is only loaded if the AI based functionality is actually supported:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"php\" class=\"language-php\">function aicig_enqueue_media_assets( string $hook_suffix ): void {\n    if ( 'upload.php' !== $hook_suffix ) {\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return;\n&nbsp;&nbsp;&nbsp;&nbsp;}\n\n&nbsp;&nbsp;&nbsp;&nbsp;if ( ! current_user_can( 'upload_files' ) ) {\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return;\n&nbsp;&nbsp;&nbsp;&nbsp;}\n\n&nbsp;&nbsp;&nbsp;&nbsp;if ( ! aicig_get_image_generation_prompt( 'test' )-&gt;is_supported_for_image_generation() ) {\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\/\/ Show an admin notice, or handle the unsupported case in another way.\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return;\n&nbsp;&nbsp;&nbsp;&nbsp;}\n\n&nbsp;&nbsp;&nbsp;&nbsp;wp_enqueue_script( 'aicig-imagegen' );\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Three checks happen in sequence:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Screen check<\/strong> \u2014 Only run on <code>upload.php<\/code> (the Media Library).<\/li>\n\n\n\n<li><strong>Capability check<\/strong> \u2014 Only for users who can upload files. It&#8217;s important to use the same capability here that the REST API endpoints use, so we gate the UI based on the same criteria. Otherwise, you may render UI for something that doesn&#8217;t actually work.<\/li>\n\n\n\n<li><strong>Support check<\/strong> \u2014 Call <code>is_supported_for_image_generation()<\/code> on a prompt builder to verify that a configured provider can handle image generation. If not, bail out early \u2014 you might want to show a notice pointing the site owner to the Connectors settings.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Notice how the same <code>aicig_get_image_generation_prompt()<\/code> function from <code>includes\/prompt.php<\/code> is reused here. Passing &#8216;test&#8217; as the prompt text is sufficient \u2014 the method only checks provider and model capabilities, not the prompt content. That is why we didn&#8217;t have it return the prompt result, so it can also be used to check for support.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The script is only enqueued when all three conditions pass. This means the &#8220;Generate Image File&#8221; button only appears when it will actually work.<\/p>\n\n\n\n<h3 id=\"the-frontend\" class=\"wp-block-heading\">The frontend<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The frontend TypeScript file is ~590 lines of vanilla DOM manipulation. It creates a modal dialog, handles form submission via <code>@wordpress\/api-fetch<\/code>, displays the generated image preview with provider\/model attribution, and handles saving to the Media Library.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Explaining that code in depth would go beyond the purpose of this article. It&#8217;s all pretty standard vanilla JavaScript functionality, so I&#8217;d encourage you to study it separately if you&#8217;re interested. So rather than walking through the frontend code in detail here, you can copy it from the GitHub repository: <code><a href=\"https:\/\/github.com\/wptrainingteam\/ai-client-imagegen\/blob\/main\/src\/index.ts\">src\/index.ts<\/a><\/code>. Place the code in your local <code>src\/index.ts<\/code> file. Once it is built, it will appear as <code>build\/index.js<\/code>, i.e. the same script path we registered above in PHP.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Just a few brief notes on the client-side code: The code uses vanilla JavaScript (well, TypeScript compiled to JavaScript) with no framework dependency beyond <code>@wordpress\/api-fetch<\/code> and <code>@wordpress\/i18n<\/code>. This is a deliberate choice \u2014 for a self-contained dialog, vanilla DOM manipulation is lightweight and performant. Modern coding agents are also particularly good at writing this kind of straightforward imperative UI code. For more complex plugin interfaces, using WordPress&#8217;s bundled version of React would probably be the better fit.<\/p>\n\n\n\n<h3 id=\"build-and-test\" class=\"wp-block-heading\">Build and test<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The plugin needs two configuration files in the root directory to build the frontend code and allow testing the plugin in a bundled WordPress development environment.<\/p>\n\n\n\n<div class=\"wp-block-wporg-notice is-info-notice\"><div class=\"wp-block-wporg-notice__icon\"><\/div><div class=\"wp-block-wporg-notice__content\"><p><strong>Note:<\/strong> You need Node.js installed on your system to use the tooling. To use the built-in development environment based on <code>@wordpress\/env<\/code>, you also need to have Docker installed. Alternatively, feel free to use another development environment of your choice.<\/p><\/div><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">First, a <code>package.json<\/code> that declares the build tooling and runtime dependencies:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"json\" class=\"language-json\">{\n    \"name\": \"ai-client-imagegen\",\n&nbsp;&nbsp;&nbsp;&nbsp;\"private\": true,\n&nbsp;&nbsp;&nbsp;&nbsp;\"license\": \"GPL-2.0-or-later\",\n&nbsp;&nbsp;&nbsp;&nbsp;\"dependencies\": {\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\"@wordpress\/api-fetch\": \"^7.41.0\",\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\"@wordpress\/i18n\": \"^6.14.0\"\n&nbsp;&nbsp;&nbsp;&nbsp;},\n&nbsp;&nbsp;&nbsp;&nbsp;\"devDependencies\": {\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\"@wordpress\/env\": \"^10.27.0\",\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\"@wordpress\/scripts\": \"^31.0.0\",\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\"typescript\": \"^5.8.2\"\n&nbsp;&nbsp;&nbsp;&nbsp;},\n&nbsp;&nbsp;&nbsp;&nbsp;\"scripts\": {\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\"build\": \"wp-scripts build\",\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\"wp-env\": \"wp-env\"\n&nbsp;&nbsp;&nbsp;&nbsp;}\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Second, a <code>.wp-env.json<\/code> file so you can spin up a local WordPress 7.0 environment with the plugin already active using <a href=\"https:\/\/developer.wordpress.org\/block-editor\/reference-guides\/packages\/packages-env\/\"><code>wp-env<\/code><\/a>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"json\" class=\"language-json\">{\n    \"core\": \"https:\/\/wordpress.org\/wordpress-7.0.zip\",\n&nbsp;&nbsp;&nbsp;&nbsp;\"plugins\": [ \".\" ]\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">With these files in place, install dependencies and build:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\">npm install\nnpm run build<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This runs <code>@wordpress\/scripts<\/code>, which compiles <code>src\/index.ts<\/code> into <code>build\/index.js<\/code> and generates the <code>build\/index.asset.php<\/code> dependency manifest. The PHP side does not need a build step \u2014 the AI Client is part of WordPress Core.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To test the plugin, start the local environment:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\">npm run wp-env start<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This launches a Docker-based WordPress site at <code>http:\/\/localhost:8888<\/code> (default credentials: admin \/ password) with the plugin already active. From there:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Go to <strong>Settings &gt; Connectors<\/strong> and configure an AI provider that supports image generation.<\/li>\n\n\n\n<li>Navigate to <strong>Media &gt; Library<\/strong>.<\/li>\n\n\n\n<li>Click the <strong>&#8220;Generate Image File&#8221;<\/strong> button, enter a prompt, and generate an image.<\/li>\n\n\n\n<li>Once you&#8217;re happy with an image, click the <strong>Save to Media Library<\/strong> button. Optionally, customize the file name to use for the image.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">If you see a notice instead of the button, your configured provider does not support image generation \u2014 check the Connectors settings.<\/p>\n\n\n\n<h2 id=\"wrapping-up\" class=\"wp-block-heading\">Wrapping up<\/h2>\n\n\n\n<h3 id=\"looking-back-the-ai-integration-is-small\" class=\"wp-block-heading\">Looking back, the AI integration is small<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If you review the plugin code, you might be surprised by how little of it is actually about AI. The prompt builder function in <code>includes\/prompt.php<\/code> is about 10 lines. The REST callback that calls <code>generate_image_result()<\/code> is a handful more. The rest is standard WordPress development \u2014 REST API registration, media handling, script enqueuing, permission checks.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That&#8217;s the point. The WordPress AI Client handles the complexity of provider communication, authentication, model selection, and response normalization. Your plugin focuses on its own purpose, and the AI part is just another API call.<\/p>\n\n\n\n<h3 id=\"going-further\" class=\"wp-block-heading\">Going further<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This article covered image generation, but this is only a basic starting point. For example, you could also allow editing an existing image with AI, e.g. where the user asks for further changes to a generated image. The prompt function for that could look almost the same as the one for generating images, only that you would in addition pass the existing image file to it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"php\" class=\"language-php\">use WordPress\\AiClient\\Files\\DTO\\File;\nuse WordPress\\AiClient\\Files\\Enums\\FileTypeEnum;\nuse WordPress\\AiClient\\Files\\Enums\\MediaOrientationEnum;\n\nfunction aicig_get_image_editing_prompt( string $prompt, File $image_file, string $orientation = '' ): WP_AI_Client_Prompt_Builder {\n&nbsp;&nbsp;&nbsp;&nbsp;$builder = wp_ai_client_prompt()\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;with_text( $prompt )\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;with_file( $image_file )\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&gt;as_output_file_type( FileTypeEnum::inline() );\n\n&nbsp;&nbsp;&nbsp;&nbsp;if ( $orientation ) {\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$builder-&gt;as_output_media_orientation( MediaOrientationEnum::from( $orientation ) );\n&nbsp;&nbsp;&nbsp;&nbsp;}\n\n&nbsp;&nbsp;&nbsp;&nbsp;return $builder;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>with_file()<\/code> call passes the source image as a <code>File<\/code> DTO. The AI Client takes care of routing this to a provider and model that supports image editing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I encourage you to take this plugin further: Maybe add the above prompt function and integrate it into the image generation REST endpoint (you only need to add an optional parameter to pass an existing image and call the above <code>aicig_get_image_editing_prompt()<\/code> function conditionally). And then explore how you could adjust the frontend UI to make this use-case intuitive.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There are so many more ideas to explore even for this seemingly simple feature. Here are some to explore:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>You could consider expanding the image generation (or editing) behavior so that it generates multiple image variations for the user&#8217;s prompt, where the user could then select their favorite.<\/li>\n\n\n\n<li>You may have noticed there&#8217;s a file name input field before uploading an image to the media library. Currently, that file name field is programmatically populated, which doesn&#8217;t lead to very useful file names. You could use the AI Client&#8217;s text generation abilities to generate a more descriptive file name based on what the image actually displays.<\/li>\n\n\n\n<li>You could integrate with existing images from your media library, e.g. show a prompt text field and a button to ask AI to edit an existing image.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">If you are curious about exploring a professional end-to-end plugin that offers AI driven features similar to this one based on the new WordPress AI client, I encourage you to take a look at the <a href=\"https:\/\/github.com\/WordPress\/ai\">official WordPress AI plugin<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You see, the abilities with AI are endless. While a simple image generation example like the one we built here is cool, it only scratches the surface. We can use AI to really bring user experience to another level.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">But with this article, you hopefully have a better idea now on how to get started with using everything the WordPress AI Client has to offer. You can explore the plugin source code on GitHub: <a href=\"https:\/\/github.com\/wptrainingteam\/ai-client-imagegen\">wptrainingteam\/ai-client-imagegen<\/a>.<\/p>\n\n\n\n<p class=\"has-text-align-right wp-block-paragraph\"><em>Props <a href='https:\/\/profiles.wordpress.org\/psykro\/' class='mention'><span class='mentions-prefix'>@<\/span>psykro<\/a> <a href='https:\/\/profiles.wordpress.org\/juanmaguitar\/' class='mention'><span class='mentions-prefix'>@<\/span>juanmaguitar<\/a> and <a href='https:\/\/profiles.wordpress.org\/bph\/' class='mention'><span class='mentions-prefix'>@<\/span>bph<\/a> for review and proofreading.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Learn how to use the WordPress AI Client to build a provider-agnostic plugin that generates images directly within the Media Library.<\/p>\n","protected":false},"author":10972453,"featured_media":6112,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"edge","default_image_id":0,"font":"","enabled":false},"version":2},"_wpas_customize_per_network":false,"jetpack_post_was_ever_published":false},"categories":[206,113,40],"tags":[217,10,55,134],"class_list":["post-6094","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai","category-common-apis","category-plugins","tag-ai","tag-extenders","tag-learning","tag-plugin-development","mentions-bph","mentions-juanmaguitar","mentions-psykro","mentions-wordpress"],"revision_note":"","jetpack_publicize_connections":[],"jetpack_featured_media_url":"https:\/\/developer.wordpress.org\/news\/files\/2026\/05\/build-an-image-generation-plugin-wp-AI-client.jpg","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/developer.wordpress.org\/news\/wp-json\/wp\/v2\/posts\/6094","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/developer.wordpress.org\/news\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/developer.wordpress.org\/news\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/developer.wordpress.org\/news\/wp-json\/wp\/v2\/users\/10972453"}],"replies":[{"embeddable":true,"href":"https:\/\/developer.wordpress.org\/news\/wp-json\/wp\/v2\/comments?post=6094"}],"version-history":[{"count":15,"href":"https:\/\/developer.wordpress.org\/news\/wp-json\/wp\/v2\/posts\/6094\/revisions"}],"predecessor-version":[{"id":6120,"href":"https:\/\/developer.wordpress.org\/news\/wp-json\/wp\/v2\/posts\/6094\/revisions\/6120"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/developer.wordpress.org\/news\/wp-json\/wp\/v2\/media\/6112"}],"wp:attachment":[{"href":"https:\/\/developer.wordpress.org\/news\/wp-json\/wp\/v2\/media?parent=6094"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/developer.wordpress.org\/news\/wp-json\/wp\/v2\/categories?post=6094"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/developer.wordpress.org\/news\/wp-json\/wp\/v2\/tags?post=6094"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}