@wp-typia/project-tools 0.22.0 → 0.22.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { quoteTsString } from "./cli-add-shared.js";
2
2
  import { buildAiFeatureEndpointManifest } from "./ai-feature-artifacts.js";
3
- import { OPTIONAL_WORDPRESS_AI_CLIENT_COMPATIBILITY, renderScaffoldCompatibilityConfig, resolveScaffoldCompatibilityPolicy, } from "./scaffold-compatibility.js";
3
+ import { OPTIONAL_WORDPRESS_AI_CLIENT_COMPATIBILITY, createScaffoldCompatibilityConfig, renderScaffoldCompatibilityConfig, resolveScaffoldCompatibilityPolicy, } from "./scaffold-compatibility.js";
4
4
  import { toPascalCase, toTitleCase } from "./string-case.js";
5
5
  function indentMultiline(source, prefix) {
6
6
  return source
@@ -79,6 +79,45 @@ export interface ${pascalCase}AiFeatureResponse {
79
79
  \tresult: ${pascalCase}AiFeatureResult;
80
80
  \ttelemetry: ${pascalCase}AiFeatureTelemetry;
81
81
  }
82
+
83
+ export type ${pascalCase}AiFeatureSupportProbeMode = 'request-time';
84
+
85
+ export type ${pascalCase}AiFeatureUnavailableErrorCode =
86
+ \t'ai_client_unavailable';
87
+
88
+ export type ${pascalCase}AiFeatureUnavailableReasonCode =
89
+ \t| 'missing-wordpress-ai-client'
90
+ \t| 'request-time-support-probe';
91
+
92
+ export interface ${pascalCase}AiFeatureSupportReason {
93
+ \tcode: ${pascalCase}AiFeatureUnavailableReasonCode;
94
+ \tlabel: string & tags.MinLength< 1 > & tags.MaxLength< 160 >;
95
+ \tmessage: string & tags.MinLength< 1 > & tags.MaxLength< 4000 >;
96
+ }
97
+
98
+ export interface ${pascalCase}AiFeatureSupportMetadata {
99
+ \tfeatureLabel: string & tags.MinLength< 1 > & tags.MaxLength< 160 >;
100
+ \tfeatureSlug: string & tags.MinLength< 1 > & tags.MaxLength< 160 >;
101
+ \tcompatibility: {
102
+ \t\thardMinimums: {
103
+ \t\t\tphp?: string;
104
+ \t\t\twordpress?: string;
105
+ \t\t};
106
+ \t\tmode: 'baseline' | 'optional' | 'required';
107
+ \t\toptionalFeatureIds: string[];
108
+ \t\toptionalFeatures: string[];
109
+ \t\trequiredFeatureIds: string[];
110
+ \t\trequiredFeatures: string[];
111
+ \t\truntimeGates: string[];
112
+ \t};
113
+ \tsupportProbe: {
114
+ \t\tendpointMethod: 'POST';
115
+ \t\tendpointPath: string & tags.MinLength< 1 > & tags.MaxLength< 200 >;
116
+ \t\tmode: ${pascalCase}AiFeatureSupportProbeMode;
117
+ \t\tunavailableErrorCode: ${pascalCase}AiFeatureUnavailableErrorCode;
118
+ \t};
119
+ \tunavailableReasons: ${pascalCase}AiFeatureSupportReason[];
120
+ }
82
121
  `;
83
122
  }
84
123
  /**
@@ -120,6 +159,8 @@ export const apiValidators = {
120
159
  */
121
160
  export function buildAiFeatureApiSource(aiFeatureSlug) {
122
161
  const pascalCase = toPascalCase(aiFeatureSlug);
162
+ const compatibility = createScaffoldCompatibilityConfig(resolveScaffoldCompatibilityPolicy(OPTIONAL_WORDPRESS_AI_CLIENT_COMPATIBILITY));
163
+ const title = toTitleCase(aiFeatureSlug);
123
164
  return `import {
124
165
  \tcallEndpoint,
125
166
  \tresolveRestRouteUrl,
@@ -127,6 +168,7 @@ export function buildAiFeatureApiSource(aiFeatureSlug) {
127
168
 
128
169
  import type {
129
170
  \t${pascalCase}AiFeatureRequest,
171
+ \t${pascalCase}AiFeatureSupportMetadata,
130
172
  } from './api-types';
131
173
  import {
132
174
  \trun${pascalCase}AiFeatureEndpoint,
@@ -153,6 +195,14 @@ function resolveRestNonce( fallback?: string ): string | undefined {
153
195
  \t\t: undefined;
154
196
  }
155
197
 
198
+ function isPlainObject( value: unknown ): value is Record< string, unknown > {
199
+ \treturn (
200
+ \t\t!! value &&
201
+ \t\ttypeof value === 'object' &&
202
+ \t\t! Array.isArray( value )
203
+ \t);
204
+ }
205
+
156
206
  export const aiFeatureRunEndpoint = {
157
207
  \t...run${pascalCase}AiFeatureEndpoint,
158
208
  \tbuildRequestOptions: () => {
@@ -168,6 +218,63 @@ export const aiFeatureRunEndpoint = {
168
218
  \t},
169
219
  };
170
220
 
221
+ export const aiFeatureSupportMetadata = {
222
+ \tcompatibility: ${JSON.stringify(compatibility, null, "\t")},
223
+ \tfeatureLabel: ${quoteTsString(title)},
224
+ \tfeatureSlug: ${quoteTsString(aiFeatureSlug)},
225
+ \tsupportProbe: {
226
+ \t\tendpointMethod: 'POST',
227
+ \t\tendpointPath: aiFeatureRunEndpoint.path,
228
+ \t\tmode: 'request-time',
229
+ \t\tunavailableErrorCode: 'ai_client_unavailable',
230
+ \t},
231
+ \tunavailableReasons: [
232
+ \t\t{
233
+ \t\t\tcode: 'missing-wordpress-ai-client',
234
+ \t\t\tlabel: 'WordPress AI Client unavailable',
235
+ \t\t\tmessage:
236
+ \t\t\t\t'This AI feature stays disabled until the WordPress AI Client is available on the site.',
237
+ \t\t},
238
+ \t\t{
239
+ \t\t\tcode: 'request-time-support-probe',
240
+ \t\t\tlabel: 'Support is checked at request time',
241
+ \t\t\tmessage:
242
+ \t\t\t\t'Support is verified when the feature runs, so editor and admin UIs should degrade gracefully when the site rejects the request.',
243
+ \t\t},
244
+ \t],
245
+ } satisfies ${pascalCase}AiFeatureSupportMetadata;
246
+
247
+ export function getAiFeatureSupportHintLines() {
248
+ \treturn aiFeatureSupportMetadata.unavailableReasons.map(
249
+ \t\t( reason ) => reason.message
250
+ \t);
251
+ }
252
+
253
+ export function isAiFeatureSupportUnavailableError( error: unknown ) {
254
+ \tif ( ! isPlainObject( error ) ) {
255
+ \t\treturn false;
256
+ \t}
257
+
258
+ \tconst data = isPlainObject( error.data ) ? error.data : undefined;
259
+ \treturn (
260
+ \t\terror.code === aiFeatureSupportMetadata.supportProbe.unavailableErrorCode ||
261
+ \t\tdata?.status === 501
262
+ \t);
263
+ }
264
+
265
+ export function resolveAiFeatureUnavailableMessage( error: unknown ) {
266
+ \tif (
267
+ \t\tisPlainObject( error ) &&
268
+ \t\ttypeof error.message === 'string' &&
269
+ \t\terror.message.length > 0
270
+ \t) {
271
+ \t\treturn error.message;
272
+ \t}
273
+
274
+ \treturn aiFeatureSupportMetadata.unavailableReasons[ 0 ]?.message ??
275
+ \t\t'This AI feature is currently unavailable.';
276
+ }
277
+
171
278
  export function runAiFeature( request: ${pascalCase}AiFeatureRequest ) {
172
279
  \treturn callEndpoint( aiFeatureRunEndpoint, request );
173
280
  }
@@ -189,6 +296,10 @@ import type {
189
296
  } from './api-types';
190
297
  import {
191
298
  \taiFeatureRunEndpoint,
299
+ \taiFeatureSupportMetadata,
300
+ \tgetAiFeatureSupportHintLines,
301
+ \tisAiFeatureSupportUnavailableError,
302
+ \tresolveAiFeatureUnavailableMessage,
192
303
  } from './api';
193
304
 
194
305
  export type UseRun${pascalCase}AiFeatureMutationOptions =
@@ -203,6 +314,13 @@ export function useRun${pascalCase}AiFeatureMutation(
203
314
  ) {
204
315
  \treturn useEndpointMutation( aiFeatureRunEndpoint, options );
205
316
  }
317
+
318
+ export {
319
+ \taiFeatureSupportMetadata,
320
+ \tgetAiFeatureSupportHintLines,
321
+ \tisAiFeatureSupportUnavailableError,
322
+ \tresolveAiFeatureUnavailableMessage,
323
+ };
206
324
  `;
207
325
  }
208
326
  /**
@@ -18,18 +18,40 @@ function buildAiFeaturePhpSource(aiFeatureSlug, namespace, phpPrefix, textDomain
18
18
  const normalizeSchemaFunctionName = `${phpPrefix}_${aiFeaturePhpId}_sanitize_ai_feature_schema`;
19
19
  const validatePayloadFunctionName = `${phpPrefix}_${aiFeaturePhpId}_validate_ai_feature_payload`;
20
20
  const canManageFunctionName = `${phpPrefix}_${aiFeaturePhpId}_can_manage_ai_feature`;
21
+ const normalizePromptPayloadFunctionName = `${phpPrefix}_${aiFeaturePhpId}_normalize_ai_feature_prompt_payload`;
21
22
  const buildPromptFunctionName = `${phpPrefix}_${aiFeaturePhpId}_build_ai_feature_prompt`;
23
+ const resolvePromptOptionsFunctionName = `${phpPrefix}_${aiFeaturePhpId}_resolve_ai_feature_prompt_options`;
22
24
  const normalizeProviderTypeFunctionName = `${phpPrefix}_${aiFeaturePhpId}_normalize_provider_type`;
23
25
  const buildTelemetryFunctionName = `${phpPrefix}_${aiFeaturePhpId}_build_ai_feature_telemetry`;
26
+ const resolveUnavailableMessageFunctionName = `${phpPrefix}_${aiFeaturePhpId}_resolve_ai_feature_unavailable_message`;
24
27
  const isSupportedFunctionName = `${phpPrefix}_${aiFeaturePhpId}_is_ai_feature_supported`;
25
28
  const adminNoticeFunctionName = `${phpPrefix}_${aiFeaturePhpId}_ai_feature_admin_notice`;
26
29
  const handlerFunctionName = `${phpPrefix}_${aiFeaturePhpId}_handle_run_ai_feature`;
27
30
  const registerRoutesFunctionName = `${phpPrefix}_${aiFeaturePhpId}_register_ai_feature_routes`;
31
+ const permissionFilterHook = `${phpPrefix}_${aiFeaturePhpId}_ai_feature_permission`;
32
+ const promptPayloadFilterHook = `${phpPrefix}_${aiFeaturePhpId}_ai_feature_prompt_payload`;
33
+ const promptFilterHook = `${phpPrefix}_${aiFeaturePhpId}_ai_feature_prompt`;
34
+ const promptOptionsFilterHook = `${phpPrefix}_${aiFeaturePhpId}_ai_feature_prompt_options`;
35
+ const adminNoticeMessageFilterHook = `${phpPrefix}_${aiFeaturePhpId}_ai_feature_admin_notice_message`;
36
+ const unavailableMessageFilterHook = `${phpPrefix}_${aiFeaturePhpId}_ai_feature_unavailable_message`;
37
+ const telemetryFilterHook = `${phpPrefix}_${aiFeaturePhpId}_ai_feature_telemetry`;
28
38
  return `<?php
29
39
  if ( ! defined( 'ABSPATH' ) ) {
30
40
  \treturn;
31
41
  }
32
42
 
43
+ /*
44
+ * Customization hooks for the ${aiFeatureTitle} AI feature:
45
+ *
46
+ * - ${quotePhpString(permissionFilterHook)} filters the default current_user_can( 'edit_posts' ) capability check.
47
+ * - ${quotePhpString(promptPayloadFilterHook)} filters the validated request payload array before prompt serialization.
48
+ * - ${quotePhpString(promptFilterHook)} filters the final prompt string after payload normalization.
49
+ * - ${quotePhpString(promptOptionsFilterHook)} filters prompt options with \`temperature\` and \`modelPreference\` keys.
50
+ * - ${quotePhpString(adminNoticeMessageFilterHook)} filters the wp-admin notice shown when AI support is unavailable.
51
+ * - ${quotePhpString(unavailableMessageFilterHook)} filters REST-facing unavailable messages by reason code.
52
+ * - ${quotePhpString(telemetryFilterHook)} filters the response telemetry array before schema validation. Return a schema-compatible array.
53
+ */
54
+
33
55
  if ( ! function_exists( '${loadSchemaFunctionName}' ) ) {
34
56
  \tfunction ${loadSchemaFunctionName}( $schema_name ) {
35
57
  \t\t$project_root = dirname( __DIR__, 2 );
@@ -96,17 +118,111 @@ if ( ! function_exists( '${validatePayloadFunctionName}' ) ) {
96
118
  }
97
119
 
98
120
  if ( ! function_exists( '${canManageFunctionName}' ) ) {
99
- \tfunction ${canManageFunctionName}() {
100
- \t\treturn current_user_can( 'edit_posts' );
121
+ \tfunction ${canManageFunctionName}( WP_REST_Request $request = null ) {
122
+ \t\t$permission = apply_filters(
123
+ \t\t\t${quotePhpString(permissionFilterHook)},
124
+ \t\t\tcurrent_user_can( 'edit_posts' ),
125
+ \t\t\t$request
126
+ \t\t);
127
+ \t\tif ( is_wp_error( $permission ) ) {
128
+ \t\t\treturn $permission;
129
+ \t\t}
130
+ \t\treturn (bool) $permission;
131
+ \t}
132
+ }
133
+
134
+ if ( ! function_exists( '${normalizePromptPayloadFunctionName}' ) ) {
135
+ \tfunction ${normalizePromptPayloadFunctionName}( array $payload ) {
136
+ \t\t$normalized_payload = apply_filters(
137
+ \t\t\t${quotePhpString(promptPayloadFilterHook)},
138
+ \t\t\t$payload
139
+ \t\t);
140
+ \t\treturn is_array( $normalized_payload ) ? $normalized_payload : $payload;
101
141
  \t}
102
142
  }
103
143
 
104
144
  if ( ! function_exists( '${buildPromptFunctionName}' ) ) {
105
145
  \tfunction ${buildPromptFunctionName}( array $payload ) {
106
- \t\treturn sprintf(
146
+ \t\t$normalized_payload = ${normalizePromptPayloadFunctionName}( $payload );
147
+ \t\t$prompt = sprintf(
107
148
  \t\t\t'You are helping with the %1$s AI workflow. Read the JSON request payload and return JSON that matches the provided schema. Request payload: %2$s',
108
149
  \t\t\t${quotePhpString(aiFeatureTitle)},
109
- \t\t\twp_json_encode( $payload )
150
+ \t\t\twp_json_encode( $normalized_payload )
151
+ \t\t);
152
+ \t\t$filtered_prompt = apply_filters(
153
+ \t\t\t${quotePhpString(promptFilterHook)},
154
+ \t\t\t$prompt,
155
+ \t\t\t$normalized_payload,
156
+ \t\t\t$payload
157
+ \t\t);
158
+ \t\treturn is_string( $filtered_prompt ) && '' !== $filtered_prompt ? $filtered_prompt : $prompt;
159
+ \t}
160
+ }
161
+
162
+ if ( ! function_exists( '${resolvePromptOptionsFunctionName}' ) ) {
163
+ \tfunction ${resolvePromptOptionsFunctionName}( array $payload = array() ) {
164
+ \t\t$options = apply_filters(
165
+ \t\t\t${quotePhpString(promptOptionsFilterHook)},
166
+ \t\t\tarray(
167
+ \t\t\t\t'modelPreference' => array(),
168
+ \t\t\t\t'temperature' => 0.2,
169
+ \t\t\t),
170
+ \t\t\t$payload
171
+ \t\t);
172
+ \t\tif ( ! is_array( $options ) ) {
173
+ \t\t\t$options = array();
174
+ \t\t}
175
+
176
+ \t\t$temperature = 0.2;
177
+ \t\tif ( array_key_exists( 'temperature', $options ) ) {
178
+ \t\t\tif ( null === $options['temperature'] ) {
179
+ \t\t\t\t$temperature = null;
180
+ \t\t\t} elseif ( is_numeric( $options['temperature'] ) ) {
181
+ \t\t\t\t$temperature = (float) $options['temperature'];
182
+ \t\t\t}
183
+ \t\t}
184
+
185
+ \t\t$model_preferences = array();
186
+ \t\tif ( isset( $options['modelPreference'] ) ) {
187
+ \t\t\t$raw_model_preferences = $options['modelPreference'];
188
+ \t\t\tif ( is_string( $raw_model_preferences ) && '' !== $raw_model_preferences ) {
189
+ \t\t\t\t$model_preferences = array( $raw_model_preferences );
190
+ \t\t\t} elseif ( is_array( $raw_model_preferences ) ) {
191
+ \t\t\t\t$model_preferences = array_values(
192
+ \t\t\t\t\tarray_filter(
193
+ \t\t\t\t\t\tarray_map(
194
+ \t\t\t\t\t\t\tstatic function ( $candidate ) {
195
+ \t\t\t\t\t\t\t\tif ( is_string( $candidate ) && '' !== $candidate ) {
196
+ \t\t\t\t\t\t\t\t\treturn $candidate;
197
+ \t\t\t\t\t\t\t\t}
198
+ \t\t\t\t\t\t\t\tif ( ! is_array( $candidate ) ) {
199
+ \t\t\t\t\t\t\t\t\treturn null;
200
+ \t\t\t\t\t\t\t\t}
201
+
202
+ \t\t\t\t\t\t\t\t$normalized = array_values(
203
+ \t\t\t\t\t\t\t\t\tarray_filter(
204
+ \t\t\t\t\t\t\t\t\t\t$candidate,
205
+ \t\t\t\t\t\t\t\t\t\tstatic function ( $value ) {
206
+ \t\t\t\t\t\t\t\t\t\t\treturn is_string( $value ) && '' !== $value;
207
+ \t\t\t\t\t\t\t\t\t\t}
208
+ \t\t\t\t\t\t\t\t\t)
209
+ \t\t\t\t\t\t\t\t);
210
+
211
+ \t\t\t\t\t\t\t\treturn count( $normalized ) > 0 ? $normalized : null;
212
+ \t\t\t\t\t\t\t},
213
+ \t\t\t\t\t\t\t$raw_model_preferences
214
+ \t\t\t\t\t\t),
215
+ \t\t\t\t\t\tstatic function ( $candidate ) {
216
+ \t\t\t\t\t\t\treturn null !== $candidate;
217
+ \t\t\t\t\t\t}
218
+ \t\t\t\t\t)
219
+ \t\t\t\t);
220
+ \t\t\t}
221
+ \t\t}
222
+
223
+ \t\treturn array(
224
+ \t\t\t'modelPreference' => $model_preferences,
225
+ \t\t\t'temperature' => $temperature,
110
226
  \t\t);
111
227
  \t}
112
228
  }
@@ -122,7 +238,7 @@ if ( ! function_exists( '${normalizeProviderTypeFunctionName}' ) ) {
122
238
  }
123
239
 
124
240
  if ( ! function_exists( '${buildTelemetryFunctionName}' ) ) {
125
- \tfunction ${buildTelemetryFunctionName}( $result ) {
241
+ \tfunction ${buildTelemetryFunctionName}( $result, array $payload = array(), array $normalized_result = array() ) {
126
242
  \t\tif (
127
243
  \t\t\t! is_object( $result ) ||
128
244
  \t\t\t! method_exists( $result, 'getId' ) ||
@@ -182,47 +298,95 @@ if ( ! function_exists( '${buildTelemetryFunctionName}' ) ) {
182
298
  \t\t\t}
183
299
  \t\t}
184
300
 
185
- \t\treturn $telemetry;
301
+ \t\t$filtered_telemetry = apply_filters(
302
+ \t\t\t${quotePhpString(telemetryFilterHook)},
303
+ \t\t\t$telemetry,
304
+ \t\t\t$result,
305
+ \t\t\t$payload,
306
+ \t\t\t$normalized_result
307
+ \t\t);
308
+ \t\treturn is_array( $filtered_telemetry ) ? $filtered_telemetry : $telemetry;
309
+ \t}
310
+ }
311
+
312
+ if ( ! function_exists( '${resolveUnavailableMessageFunctionName}' ) ) {
313
+ \tfunction ${resolveUnavailableMessageFunctionName}( $message, $reason, array $context = array() ) {
314
+ \t\t$filtered_message = apply_filters(
315
+ \t\t\t${quotePhpString(unavailableMessageFilterHook)},
316
+ \t\t\t$message,
317
+ \t\t\t$reason,
318
+ \t\t\t$context
319
+ \t\t);
320
+ \t\treturn is_string( $filtered_message ) && '' !== $filtered_message ? $filtered_message : $message;
186
321
  \t}
187
322
  }
188
323
 
189
324
  if ( ! function_exists( '${isSupportedFunctionName}' ) ) {
190
- \tfunction ${isSupportedFunctionName}() {
325
+ \tfunction ${isSupportedFunctionName}( array $payload = array(), $cache_result = true ) {
191
326
  \t\tstatic $is_supported = null;
192
- \t\tif ( null !== $is_supported ) {
327
+ \t\t$use_cache = $cache_result && count( $payload ) === 0;
328
+ \t\tif ( $use_cache && null !== $is_supported ) {
193
329
  \t\t\treturn $is_supported;
194
330
  \t\t}
195
331
 
196
332
  \t\tif ( ! function_exists( 'wp_ai_client_prompt' ) ) {
197
- \t\t\t$is_supported = false;
198
- \t\t\treturn $is_supported;
333
+ \t\t\tif ( $use_cache ) {
334
+ \t\t\t\t$is_supported = false;
335
+ \t\t\t}
336
+ \t\t\treturn false;
199
337
  \t\t}
200
338
 
201
339
  \t\t$schema = ${loadAiSchemaFunctionName}();
202
340
  \t\tif ( ! is_array( $schema ) ) {
203
- \t\t\t$is_supported = false;
204
- \t\t\treturn $is_supported;
341
+ \t\t\tif ( $use_cache ) {
342
+ \t\t\t\t$is_supported = false;
343
+ \t\t\t}
344
+ \t\t\treturn false;
205
345
  \t\t}
206
346
 
207
347
  \t\t$prompt = wp_ai_client_prompt( 'AI feature support probe.' );
208
348
  \t\tif ( ! is_object( $prompt ) || ! method_exists( $prompt, 'as_json_response' ) ) {
209
- \t\t\t$is_supported = false;
210
- \t\t\treturn $is_supported;
349
+ \t\t\tif ( $use_cache ) {
350
+ \t\t\t\t$is_supported = false;
351
+ \t\t\t}
352
+ \t\t\treturn false;
353
+ \t\t}
354
+ \t\t$prompt_options = ${resolvePromptOptionsFunctionName}( $payload );
355
+ \t\tif (
356
+ \t\t\tarray_key_exists( 'temperature', $prompt_options ) &&
357
+ \t\t\tnull !== $prompt_options['temperature'] &&
358
+ \t\t\tmethod_exists( $prompt, 'using_temperature' )
359
+ \t\t) {
360
+ \t\t\t$prompt = $prompt->using_temperature( $prompt_options['temperature'] );
361
+ \t\t}
362
+ \t\tif (
363
+ \t\t\t! empty( $prompt_options['modelPreference'] ) &&
364
+ \t\t\tmethod_exists( $prompt, 'using_model_preference' )
365
+ \t\t) {
366
+ \t\t\t$prompt = $prompt->using_model_preference( ...$prompt_options['modelPreference'] );
211
367
  \t\t}
212
368
 
213
369
  \t\t$structured_prompt = $prompt->as_json_response( $schema );
214
370
  \t\tif ( ! is_object( $structured_prompt ) ) {
215
- \t\t\t$is_supported = false;
216
- \t\t\treturn $is_supported;
371
+ \t\t\tif ( $use_cache ) {
372
+ \t\t\t\t$is_supported = false;
373
+ \t\t\t}
374
+ \t\t\treturn false;
217
375
  \t\t}
218
376
 
219
377
  \t\tif ( method_exists( $structured_prompt, 'is_supported_for_text_generation' ) ) {
220
- \t\t\t$is_supported = (bool) $structured_prompt->is_supported_for_text_generation();
221
- \t\t\treturn $is_supported;
378
+ \t\t\t$supported = (bool) $structured_prompt->is_supported_for_text_generation();
379
+ \t\t\tif ( $use_cache ) {
380
+ \t\t\t\t$is_supported = $supported;
381
+ \t\t\t}
382
+ \t\t\treturn $supported;
222
383
  \t\t}
223
384
 
224
- \t\t$is_supported = method_exists( $structured_prompt, 'generate_text_result' );
225
- \t\treturn $is_supported;
385
+ \t\t$supported = method_exists( $structured_prompt, 'generate_text_result' );
386
+ \t\tif ( $use_cache ) {
387
+ \t\t\t$is_supported = $supported;
388
+ \t\t}
389
+ \t\treturn $supported;
226
390
  \t}
227
391
  }
228
392
 
@@ -237,6 +401,18 @@ if ( ! function_exists( '${adminNoticeFunctionName}' ) ) {
237
401
  \t\t\t__( 'The %s AI feature is optional and remains disabled until the WordPress AI Client is available with structured text generation support for the generated schema.', ${quotePhpString(textDomain)} ),
238
402
  \t\t\t${quotePhpString(aiFeatureTitle)}
239
403
  \t\t);
404
+ \t\t$filtered_message = apply_filters(
405
+ \t\t\t${quotePhpString(adminNoticeMessageFilterHook)},
406
+ \t\t\t$message,
407
+ \t\t\tarray(
408
+ \t\t\t\t'featureSlug' => ${quotePhpString(aiFeatureSlug)},
409
+ \t\t\t\t'featureTitle' => ${quotePhpString(aiFeatureTitle)},
410
+ \t\t\t\t'namespace' => ${quotePhpString(namespace)},
411
+ \t\t\t)
412
+ \t\t);
413
+ \t\tif ( is_string( $filtered_message ) && '' !== $filtered_message ) {
414
+ \t\t\t$message = $filtered_message;
415
+ \t\t}
240
416
  \t\tprintf( '<div class="notice notice-warning"><p>%s</p></div>', esc_html( $message ) );
241
417
  \t}
242
418
  }
@@ -248,10 +424,16 @@ if ( ! function_exists( '${handlerFunctionName}' ) ) {
248
424
  \t\t\treturn $payload;
249
425
  \t\t}
250
426
 
251
- \t\tif ( ! ${isSupportedFunctionName}() ) {
427
+ \t\tif ( ! ${isSupportedFunctionName}( $payload, false ) ) {
252
428
  \t\t\treturn new WP_Error(
253
429
  \t\t\t\t'ai_client_unavailable',
254
- \t\t\t\t'The WordPress AI Client is unavailable or does not support this feature endpoint.',
430
+ \t\t\t\t${resolveUnavailableMessageFunctionName}(
431
+ \t\t\t\t\t'The WordPress AI Client is unavailable or does not support this feature endpoint.',
432
+ \t\t\t\t\t'support_probe_failed',
433
+ \t\t\t\t\tarray(
434
+ \t\t\t\t\t\t'featureSlug' => ${quotePhpString(aiFeatureSlug)},
435
+ \t\t\t\t\t)
436
+ \t\t\t\t),
255
437
  \t\t\t\tarray( 'status' => 501 )
256
438
  \t\t\t);
257
439
  \t\t}
@@ -265,22 +447,45 @@ if ( ! function_exists( '${handlerFunctionName}' ) ) {
265
447
  \t\t\t);
266
448
  \t\t}
267
449
 
450
+ \t\t$prompt_options = ${resolvePromptOptionsFunctionName}( $payload );
268
451
  \t\t$prompt = wp_ai_client_prompt( ${buildPromptFunctionName}( $payload ) );
269
452
  \t\tif ( ! is_object( $prompt ) ) {
270
453
  \t\t\treturn new WP_Error(
271
454
  \t\t\t\t'ai_client_unavailable',
272
- \t\t\t\t'The WordPress AI Client prompt builder is unavailable on this site.',
455
+ \t\t\t\t${resolveUnavailableMessageFunctionName}(
456
+ \t\t\t\t\t'The WordPress AI Client prompt builder is unavailable on this site.',
457
+ \t\t\t\t\t'prompt_builder_missing',
458
+ \t\t\t\t\tarray(
459
+ \t\t\t\t\t\t'featureSlug' => ${quotePhpString(aiFeatureSlug)},
460
+ \t\t\t\t\t)
461
+ \t\t\t\t),
273
462
  \t\t\t\tarray( 'status' => 501 )
274
463
  \t\t\t);
275
464
  \t\t}
276
465
 
277
- \t\tif ( method_exists( $prompt, 'using_temperature' ) ) {
278
- \t\t\t$prompt = $prompt->using_temperature( 0.2 );
466
+ \t\tif (
467
+ \t\t\tarray_key_exists( 'temperature', $prompt_options ) &&
468
+ \t\t\tnull !== $prompt_options['temperature'] &&
469
+ \t\t\tmethod_exists( $prompt, 'using_temperature' )
470
+ \t\t) {
471
+ \t\t\t$prompt = $prompt->using_temperature( $prompt_options['temperature'] );
472
+ \t\t}
473
+ \t\tif (
474
+ \t\t\t! empty( $prompt_options['modelPreference'] ) &&
475
+ \t\t\tmethod_exists( $prompt, 'using_model_preference' )
476
+ \t\t) {
477
+ \t\t\t$prompt = $prompt->using_model_preference( ...$prompt_options['modelPreference'] );
279
478
  \t\t}
280
479
  \t\tif ( ! method_exists( $prompt, 'as_json_response' ) ) {
281
480
  \t\t\treturn new WP_Error(
282
481
  \t\t\t\t'ai_client_unavailable',
283
- \t\t\t\t'The current WordPress AI Client does not expose as_json_response().',
482
+ \t\t\t\t${resolveUnavailableMessageFunctionName}(
483
+ \t\t\t\t\t'The current WordPress AI Client does not expose as_json_response().',
484
+ \t\t\t\t\t'as_json_response_missing',
485
+ \t\t\t\t\tarray(
486
+ \t\t\t\t\t\t'featureSlug' => ${quotePhpString(aiFeatureSlug)},
487
+ \t\t\t\t\t)
488
+ \t\t\t\t),
284
489
  \t\t\t\tarray( 'status' => 501 )
285
490
  \t\t\t);
286
491
  \t\t}
@@ -289,7 +494,13 @@ if ( ! function_exists( '${handlerFunctionName}' ) ) {
289
494
  \t\tif ( ! is_object( $structured_prompt ) ) {
290
495
  \t\t\treturn new WP_Error(
291
496
  \t\t\t\t'ai_client_unavailable',
292
- \t\t\t\t'The current WordPress AI Client could not prepare a structured-output prompt.',
497
+ \t\t\t\t${resolveUnavailableMessageFunctionName}(
498
+ \t\t\t\t\t'The current WordPress AI Client could not prepare a structured-output prompt.',
499
+ \t\t\t\t\t'structured_prompt_missing',
500
+ \t\t\t\t\tarray(
501
+ \t\t\t\t\t\t'featureSlug' => ${quotePhpString(aiFeatureSlug)},
502
+ \t\t\t\t\t)
503
+ \t\t\t\t),
293
504
  \t\t\t\tarray( 'status' => 501 )
294
505
  \t\t\t);
295
506
  \t\t}
@@ -300,14 +511,26 @@ if ( ! function_exists( '${handlerFunctionName}' ) ) {
300
511
  \t\t) {
301
512
  \t\t\treturn new WP_Error(
302
513
  \t\t\t\t'ai_client_unavailable',
303
- \t\t\t\t'The current WordPress AI Client provider or model does not support this structured-output feature.',
514
+ \t\t\t\t${resolveUnavailableMessageFunctionName}(
515
+ \t\t\t\t\t'The current WordPress AI Client provider or model does not support this structured-output feature.',
516
+ \t\t\t\t\t'text_generation_unsupported',
517
+ \t\t\t\t\tarray(
518
+ \t\t\t\t\t\t'featureSlug' => ${quotePhpString(aiFeatureSlug)},
519
+ \t\t\t\t\t)
520
+ \t\t\t\t),
304
521
  \t\t\t\tarray( 'status' => 501 )
305
522
  \t\t\t);
306
523
  \t\t}
307
524
  \t\tif ( ! method_exists( $structured_prompt, 'generate_text_result' ) ) {
308
525
  \t\t\treturn new WP_Error(
309
526
  \t\t\t\t'ai_client_unavailable',
310
- \t\t\t\t'The current WordPress AI Client does not expose generate_text_result() after as_json_response().',
527
+ \t\t\t\t${resolveUnavailableMessageFunctionName}(
528
+ \t\t\t\t\t'The current WordPress AI Client does not expose generate_text_result() after as_json_response().',
529
+ \t\t\t\t\t'generate_text_result_missing',
530
+ \t\t\t\t\tarray(
531
+ \t\t\t\t\t\t'featureSlug' => ${quotePhpString(aiFeatureSlug)},
532
+ \t\t\t\t\t)
533
+ \t\t\t\t),
311
534
  \t\t\t\tarray( 'status' => 501 )
312
535
  \t\t\t);
313
536
  \t\t}
@@ -342,7 +565,7 @@ if ( ! function_exists( '${handlerFunctionName}' ) ) {
342
565
  \t\t\t);
343
566
  \t\t}
344
567
 
345
- \t\t$telemetry = ${buildTelemetryFunctionName}( $result );
568
+ \t\t$telemetry = ${buildTelemetryFunctionName}( $result, $payload, $normalized_result );
346
569
  \t\tif ( is_wp_error( $telemetry ) ) {
347
570
  \t\t\treturn $telemetry;
348
571
  \t\t}
@@ -24,6 +24,21 @@ export declare const DEFAULT_WORDPRESS_CORE_DATA_VERSION = "^7.44.0";
24
24
  export declare const DEFAULT_WORDPRESS_DATA_VERSION = "^9.28.0";
25
25
  export declare const DEFAULT_WORDPRESS_DATAVIEWS_VERSION = "^14.1.0";
26
26
  export declare const DEFAULT_WP_TYPIA_DATAVIEWS_VERSION = "^0.1.0";
27
+ /**
28
+ * Resolve a managed package version range from linked workspace packages first,
29
+ * then installed package manifests, while preserving the shared normalization
30
+ * and manifest fingerprinting rules used by `getPackageVersions()`.
31
+ *
32
+ * @param packageName npm package whose manifest version should be consulted.
33
+ * @param fallback Canonical range to use when no usable manifest version exists.
34
+ * @param workspacePackageDirName Optional sibling monorepo package directory name.
35
+ * @returns A normalized semver range suitable for generated dependency entries.
36
+ */
37
+ export declare function resolveManagedPackageVersionRange(options: {
38
+ fallback: string;
39
+ packageName: string;
40
+ workspacePackageDirName?: string;
41
+ }): string;
27
42
  /**
28
43
  * Clears the in-memory cache used by `getPackageVersions()`.
29
44
  *