WordPress a un client d’IA intégré depuis la version 7.0, avec un filtre, wp_ai_client_prevent_prompt, qui s’exécute avant qu’un prompt quitte votre site. Retournez true pour les utilisateurs qui n’ont pas la capacité de votre choix, et les fonctions d’IA sont désactivées pour eux, peu importe le nombre de rédacteurs que vous avez. Aucun réglage de plugin n’est nécessaire, et cela couvre tous les plugins qui utilisent le client.
Le même client déclenche une action après chaque requête terminée, avec le nombre de jetons rapporté par le fournisseur. Vous pouvez donc compter l’utilisation par utilisateur et par site, et bloquer les prompts une fois qu’un plafond quotidien ou mensuel est atteint. Voici un petit plugin à chargement obligatoire (must-use) qui fait les deux, testé sur WordPress 7.1, suivi des lacunes qu’il vous reste à combler chez le fournisseur.
Key takeaways
- WordPress 7.0’s built-in AI Client lets you control AI use from one place. The wp_ai_client_prevent_prompt filter runs before any prompt leaves your site, and it covers every plugin that uses the client. arvindkumar.ca
- Core runs a fixed order of hooks on every generating call, and when the prevent filter returns true the provider is never contacted. The generate methods return a WP_Error with the code prompt_prevented and a 503 status. arvindkumar.ca
- To limit AI by role, create a must-use plugin and grant a custom capability such as use_ai to the roles you choose. Users without it are blocked, and you can swap core’s generic error for a message that makes sense to an editor. arvindkumar.ca
- The after-request action reports the token count, so you can count requests and tokens per user and per site. Once a daily or monthly cap is reached, further prompts are stopped. arvindkumar.ca
- A simple dashboard widget for users with manage_options can show administrators the numbers: the month’s total against the token cap and today’s requests by user. arvindkumar.ca
- Also set a spending limit at the provider. Set it to a number you would be annoyed to pay, use one key per site, and revoke keys for plugins you remove. arvindkumar.ca
- The AI Client hooks do not cover plugins that call a provider directly with their own API key and wp_remote_post(). For those, the provider cap is the only control. arvindkumar.ca
Summarised from this post, formatted for answer engines.
Comment les requêtes d’IA circulent-elles dans WordPress 7.0?
Trois éléments : un fournisseur, un appelant et les hooks entre les deux. Un administrateur installe un plugin de fournisseur, comme AI Provider for OpenAI, AI Provider for Google ou AI Provider for Anthropic, et saisit une clé sous Réglages, Connecteurs. Le cœur la stocke dans une option nommée connectors_ai_openai_api_key (le modèle est le même pour chaque fournisseur) ou la lit depuis une constante ou une variable d’environnement comme OPENAI_API_KEY. Ensuite, un plugin demande quelque chose : wp_ai_client_prompt( $text )->generate_text(). Le plugin officiel AI fait exactement cela dans ses capacités (abilities), que l’éditeur de blocs exécute par l’API REST. Le côté PHP est donc l’endroit où tout se rejoint.
Dans chaque appel de génération, le cœur exécute ceci dans l’ordre :
| Hook | Type | Moment d’exécution | Ce que vous recevez |
|---|---|---|---|
wp_supports_ai | filtre | En premier. La constante WP_AI_SUPPORT est vérifiée avant lui. | Un booléen. False désactive l’IA pour tout le site. |
wp_ai_client_prevent_prompt | filtre | Avant le choix d’un modèle. S’exécute aussi pour les vérifications is_supported_*. | $prevent (false) et une copie du constructeur de prompt. |
wp_ai_client_before_generate_result | action | Après le choix d’un modèle, avant la requête HTTP. | Un événement avec les messages, le modèle et la capacité. |
wp_ai_client_after_generate_result | action | Après la réponse du fournisseur. | Un événement dont getResult() inclut getTokenUsage(). |
Quand le filtre retourne true, les méthodes de génération retournent un WP_Error avec le code prompt_prevented, le message « Prompt execution was prevented by a filter. » et le statut 503, les méthodes is_supported retournent false, et le fournisseur n’est jamais contacté. Tout cela se passe dans la requête qui a demandé le prompt, donc current_user_can() vous indique qui fait la demande. La note de développement sur le client d’IA de 7.0, sur make.wordpress.org, montre le filtre qui bloque tout le monde sauf les administrateurs; ce qui suit reprend la même idée avec une capacité que vous pouvez attribuer, plus la comptabilisation.
Étape 1 : bloquer les prompts pour les utilisateurs sans capacité
Créez wp-content/mu-plugins/mysite-ai-limits.php. Les plugins must-use se chargent avant les plugins normaux et ne peuvent pas être désactivés depuis l’administration, ce qui est exactement ce qu’il faut pour un garde-fou budgétaire. Commencez par une capacité. Les rôles sont stockés dans la base de données : attribuez-la une fois, puis laissez un plugin de gestion des rôles s’en occuper par la suite. Ce code l’accorde aux administrateurs et aux rédacteurs (Editors); modifiez la liste selon vos besoins.
<?php
/**
* Plugin Name: AI access and usage limits
* Description: Allows AI Client prompts only for users with the use_ai capability, counts usage and enforces caps.
*/
use WordPress\AiClient\Events\AfterGenerateResultEvent;
/**
* Grants use_ai to Administrators and Editors, once. Edit the list, or move
* the capability around later with a role editor plugin.
*/
add_action( 'init', 'mysite_ai_grant_capability' );
function mysite_ai_grant_capability() {
if ( get_option( 'mysite_ai_caps_granted' ) ) {
return;
}
foreach ( array( 'administrator', 'editor' ) as $role_name ) {
$role = get_role( $role_name );
if ( $role ) {
$role->add_cap( 'use_ai' );
}
}
update_option( 'mysite_ai_caps_granted', 1 );
}
La capacité apparaît dans les plugins de gestion des rôles comme n’importe quelle autre, ce qui répond à la question du forum sur l’absence de capacité d’IA à cocher. Une seule personne peut être ajoutée sans toucher aux rôles : $user->add_cap( 'use_ai' ).
Passons au filtre. Il n’accepte que true ou false, donc le cœur rédige lui-même le message d’erreur, et « Prompt execution was prevented by a filter. » n’est pas ce qu’un rédacteur a besoin de lire. La solution : mémoriser la raison du blocage et substituer votre propre message à la création du WP_Error, avec l’action wp_error_added. Le bloc suivant fait les deux.
/**
* Blocks prompts for users without use_ai, and for anyone over a cap.
*/
add_filter( 'wp_ai_client_prevent_prompt', 'mysite_ai_prevent_prompt', 10, 2 );
function mysite_ai_prevent_prompt( $prevent, $builder ) {
if ( $prevent ) {
return true;
}
// Cron and WP-CLI run with no user. This lets them through, counted against the site cap only.
$is_background = wp_doing_cron() || ( defined( 'WP_CLI' ) && WP_CLI );
if ( ! $is_background && ! current_user_can( 'use_ai' ) ) {
mysite_ai_block_reason( __( 'AI features are not enabled for your account. Ask an administrator for access.', 'mysite' ) );
return true;
}
$reason = mysite_ai_over_limit( get_current_user_id(), $is_background );
if ( '' !== $reason ) {
mysite_ai_block_reason( $reason );
return true;
}
return false;
}
/**
* Remembers why the current request was blocked, so the error can say so.
*/
function mysite_ai_block_reason( $new_reason = null ) {
static $reason = '';
if ( is_string( $new_reason ) ) {
$reason = $new_reason;
}
return $reason;
}
/**
* Core's message is "Prompt execution was prevented by a filter." Swap in ours.
* unsupported_model is the code the AI plugin uses when is_supported_* says no.
*/
add_action( 'wp_error_added', 'mysite_ai_explain_block', 10, 4 );
function mysite_ai_explain_block( $code, $message, $data, $error ) {
$reason = mysite_ai_block_reason();
if ( '' !== $reason && in_array( $code, array( 'prompt_prevented', 'unsupported_model' ), true ) ) {
$error->errors[ $code ] = array( $reason );
mysite_ai_block_reason( '' );
}
}
Trois détails. L’argument $builder est une copie du constructeur de prompt sans accesseurs, donc vous ne pouvez pas lire le texte du prompt; décidez plutôt selon l’utilisateur et le contexte, et n’appelez pas ses méthodes is_supported ou generate dans le filtre, car cela relancerait le filtre. Cron et WP-CLI n’ont pas d’utilisateur connecté; cette version les laisse passer et les compte dans le plafond du site, et vous pouvez supprimer cette ligne si vous voulez bloquer aussi les tâches en arrière-plan. Enfin, le code unsupported_model est là pour le plugin AI : il appelle is_supported_for_text_generation() avant de générer, obtient false, et retourne son propre message sur le « fournisseur connecté ». En remplaçant aussi ce message, ses utilisateurs lisent votre explication.
Étape 2 : compter l’utilisation et appliquer les plafonds
L’action « after » vous remet un GenerativeAiResult, et getTokenUsage()->getTotalTokens() est le total des jetons du prompt et de la réponse rapporté par le fournisseur. Comptez aussi les requêtes, pour qu’un résultat sans jetons rapportés soit tout de même enregistré. Ce bloc conserve un total mensuel pour le site et les totaux du jour par utilisateur dans deux options qui se réinitialisent d’elles-mêmes au changement de période, et il définit les plafonds que l’étape 1 vérifie. Zéro désactive un plafond, et le filtre mysite_ai_limits permet de modifier les valeurs de n’importe où.
/**
* The caps. Zero disables a cap. Override with the mysite_ai_limits filter.
*/
function mysite_ai_limits() {
return apply_filters(
'mysite_ai_limits',
array(
'site_monthly_tokens' => 2000000,
'site_monthly_requests' => 3000,
'user_daily_requests' => 50,
)
);
}
/**
* This month's site-wide usage. Resets itself when the month changes.
*/
function mysite_ai_site_usage() {
$usage = get_option( 'mysite_ai_site_usage' );
$month = current_time( 'Y-m' );
if ( ! is_array( $usage ) || ! isset( $usage['period'] ) || $usage['period'] !== $month ) {
$usage = array(
'period' => $month,
'requests' => 0,
'tokens' => 0,
);
}
return $usage;
}
/**
* Today's usage per user ID. Resets itself when the day changes.
*/
function mysite_ai_today_usage() {
$usage = get_option( 'mysite_ai_today_usage' );
$day = current_time( 'Y-m-d' );
if ( ! is_array( $usage ) || ! isset( $usage['period'] ) || $usage['period'] !== $day ) {
$usage = array(
'period' => $day,
'users' => array(),
);
}
return $usage;
}
/**
* Returns a message when a cap has been reached, or an empty string.
*/
function mysite_ai_over_limit( $user_id, $is_background ) {
$limits = mysite_ai_limits();
$site = mysite_ai_site_usage();
if ( ( $limits['site_monthly_tokens'] > 0 && $site['tokens'] >= $limits['site_monthly_tokens'] )
|| ( $limits['site_monthly_requests'] > 0 && $site['requests'] >= $limits['site_monthly_requests'] ) ) {
return __( 'This site has reached its monthly AI budget. It resets on the first of the month.', 'mysite' );
}
if ( ! $is_background && $limits['user_daily_requests'] > 0 ) {
$today = mysite_ai_today_usage();
$used = isset( $today['users'][ $user_id ]['requests'] ) ? $today['users'][ $user_id ]['requests'] : 0;
if ( $used >= $limits['user_daily_requests'] ) {
return __( 'You have reached your daily AI limit. It resets at midnight.', 'mysite' );
}
}
return '';
}
/**
* Counts every completed request and its tokens, for the site and for the user.
*/
add_action( 'wp_ai_client_after_generate_result', 'mysite_ai_record_usage' );
function mysite_ai_record_usage( AfterGenerateResultEvent $event ) {
$tokens = $event->getResult()->getTokenUsage()->getTotalTokens();
$user_id = get_current_user_id();
$site = mysite_ai_site_usage();
++$site['requests'];
$site['tokens'] += $tokens;
update_option( 'mysite_ai_site_usage', $site, false );
$today = mysite_ai_today_usage();
if ( ! isset( $today['users'][ $user_id ] ) ) {
$today['users'][ $user_id ] = array(
'requests' => 0,
'tokens' => 0,
);
}
++$today['users'][ $user_id ]['requests'];
$today['users'][ $user_id ]['tokens'] += $tokens;
update_option( 'mysite_ai_today_usage', $today, false );
}
Une réserve. Les compteurs sont lus, modifiés puis réécrits, donc deux requêtes qui arrivent au même instant peuvent sous-compter d’une unité. C’est acceptable pour un garde-fou budgétaire. La facture, c’est le tableau de bord du fournisseur.
Étape 3 : afficher les chiffres aux administrateurs
Un widget de tableau de bord suffit. Celui-ci affiche le total du mois par rapport au plafond de jetons et les requêtes du jour par utilisateur, pour quiconque a manage_options.
/**
* A dashboard widget for administrators: this month's totals and today's users.
*/
add_action( 'wp_dashboard_setup', 'mysite_ai_dashboard_widget' );
function mysite_ai_dashboard_widget() {
if ( current_user_can( 'manage_options' ) ) {
wp_add_dashboard_widget( 'mysite_ai_usage', __( 'AI usage', 'mysite' ), 'mysite_ai_render_usage' );
}
}
function mysite_ai_render_usage() {
$limits = mysite_ai_limits();
$site = mysite_ai_site_usage();
$today = mysite_ai_today_usage();
printf(
'<p>%s</p>',
esc_html(
sprintf(
/* translators: 1: month, 2: requests, 3: tokens used, 4: token cap */
__( '%1$s so far: %2$s requests, %3$s of %4$s tokens.', 'mysite' ),
$site['period'],
number_format_i18n( $site['requests'] ),
number_format_i18n( $site['tokens'] ),
number_format_i18n( $limits['site_monthly_tokens'] )
)
)
);
if ( empty( $today['users'] ) ) {
echo '<p>' . esc_html__( 'No AI requests yet today.', 'mysite' ) . '</p>';
return;
}
uasort(
$today['users'],
function ( $a, $b ) {
return $b['requests'] <=> $a['requests'];
}
);
echo '<table class="widefat"><thead><tr><th>' . esc_html__( 'User, today', 'mysite' ) . '</th><th>' . esc_html__( 'Requests', 'mysite' ) . '</th><th>' . esc_html__( 'Tokens', 'mysite' ) . '</th></tr></thead><tbody>';
foreach ( $today['users'] as $user_id => $usage ) {
$user = get_userdata( $user_id );
$name = $user ? $user->display_name : __( 'Background (no user)', 'mysite' );
printf(
'<tr><td>%s</td><td>%s</td><td>%s</td></tr>',
esc_html( $name ),
esc_html( number_format_i18n( $usage['requests'] ) ),
esc_html( number_format_i18n( $usage['tokens'] ) )
);
}
echo '</tbody></table>';
}
Avec les quatre blocs dans le fichier, un auteur (Author) qui essaie une fonction d’IA reçoit un 503 avec « AI features are not enabled for your account », un rédacteur passe et apparaît dans le widget, et un rédacteur qui dépasse le plafond quotidien est refusé avec le message quotidien. Ce sont les cas sur lesquels ce code a été testé avec WordPress 7.1, en PHP et par l’API REST.
Étape 4 : fixer aussi une limite de dépenses chez le fournisseur
Ceinture et bretelles. La console de facturation du fournisseur vous permet de définir un budget mensuel avec des alertes, et en général une limite stricte, sur le projet auquel la clé appartient. Fixez-la au montant que vous seriez fâché de payer, pas à celui que vous prévoyez payer. Utilisez une clé par site, pour qu’un site qui s’emballe ne puisse pas dépenser le budget d’un autre, et révoquez la clé quand vous retirez un plugin qui l’a stockée. Le filtre ci-dessus bloque les prompts demandés par votre code WordPress; le plafond du fournisseur bloque tout le reste, y compris une clé qui aurait fuité.
Qu’est-ce que ça ne couvre pas?
- Les extensions qui ont leur propre champ de clé d’API et qui appellent un fournisseur avec
wp_remote_post(). Les hooks du AI Client ne voient jamais ces requêtes; seule la couche HTTP les voit, parpre_http_request, et c’est ainsi que fonctionne l’expérience Connector Approvals de l’extension AI. Pour ces extensions, le plafond côté fournisseur est le seul contrôle. - Les vérifications de clé sur l’écran Connectors passent par la vérification de disponibilité du fournisseur, et non par le constructeur de prompts. Elles ne sont donc ni bloquées ni comptées.
- Les requêtes REST. WordPress définit l’utilisateur courant avant l’exécution d’un callback, alors la vérification de capacité fonctionne avec l’authentification par témoin (cookie) et par mot de passe d’application. Une route ouverte au public s’exécute en tant qu’utilisateur 0 et est bloquée.
- Le multisite. Les rôles et les options sont propres à chaque site, donc les autorisations et les plafonds le sont aussi. Les super administrateurs passent toutes les vérifications de capacité, alors
use_aine les bloque jamais; les plafonds, eux, s’appliquent quand même. - Désactiver l’IA pour un site entier.
define( 'WP_AI_SUPPORT', false )dans wp-config.php fait retourner false àwp_supports_ai(), et le cœur de WordPress ne met alors même pas en place les connecteurs des fournisseurs d’IA.
L’équipe de l’extension AI suit le contrôle des rôles par fonctionnalité dans le ticket 736 et les mesures de protection de l’usage dans le ticket 345 sur GitHub, et le ticket Trac 65340 demande au cœur d’avertir les gens des coûts avant qu’ils collent une clé. En attendant, déposez le fichier ci-dessus dans mu-plugins, choisissez vos plafonds, réglez le plafond chez le fournisseur et surveillez le widget pendant une semaine avant de décider si les chiffres sont les bons.
