PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.12
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.12
1.1.12 1.1.11 1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 All 36 releases
← All changes | includes/widgets/widget-drafts.php +82 -10 1.0.0 → 1.1.12 View file →
@@ -169,11 +169,11 @@
169 169 * @param WP_Post $post The draft being described.
170 170 * @return string
171 171 */
172 172 function openstation_drafts_ai_instructions( WP_Post $post ) {
173 - $instructions = 'You are a writing assistant for a WordPress author. Given a draft post\'s current title and content, help them finish and file it. Provide: exactly 3 concise, compelling title options (about 70 characters max each); one 1-2 sentence excerpt suitable as the post summary; 3 to 6 lowercase topical tags; 1 to 2 categories (strongly prefer the site\'s existing categories listed above — only propose a new concise name if none fit); and a readiness check.
173 + $instructions = 'You are a writing assistant for a WordPress author. Given a draft post\'s current title and content, help them finish and file it. Provide: exactly 3 concise, compelling title options (about 70 characters max each); one 1-2 sentence excerpt suitable as the post summary; 3 to 6 lowercase topical tags; 1 to 2 categories (prefer the site\'s existing categories, listed in the message; propose a new concise name only if none fit); and a readiness check.
174 174
175 -The readiness check MUST be strict and evidence-based. Judge only STRUCTURE and COMPLETENESS: does the draft have a clear introduction, enough substance/depth, at least one concrete example or detail, and a conclusion? The "missing" array lists only what is GENUINELY ABSENT from the text you were given. CRITICAL: never invent, guess, or hallucinate problems. Do NOT claim there are typos, misspellings, or cut-off/incomplete sentences unless you can quote the exact offending text verbatim from the draft — if you are not quoting real text, do not mention it. If the draft already has an intro, body with a concrete detail, and a conclusion and reads as complete, return an EMPTY "missing" array and say it looks ready in the summary.
175 +The readiness check judges structure and completeness only: does the draft have a clear introduction, enough substance, at least one concrete example or detail, and a conclusion? The "missing" array lists only what is absent from the text you were given. Mention a typo, misspelling or cut-off sentence only together with the exact text, quoted from the draft. When the draft has an introduction, a body with a concrete detail and a conclusion, return an empty "missing" array and say in the summary that it looks ready.
176 176
177 177 Write everything in the same language as the draft. Do not invent facts that are not supported by the content.';
178 178
179 179 /**
@@ -317,22 +317,26 @@
317 317 // `generate_text()` with a JSON schema — same call shape the comment
318 318 // scorer uses. The SDK can throw as well as return a WP_Error, so both
319 319 // paths land on the same 502.
320 320 try {
321 - $json = wp_ai_client_prompt( openstation_drafts_ai_prompt_text( $post ) )
321 + $builder = wp_ai_client_prompt( openstation_drafts_ai_prompt_text( $post ) )
322 322 ->using_system_instruction( openstation_drafts_ai_instructions( $post ) )
323 - ->as_json_response( openstation_ai_normalize_response_schema( openstation_drafts_ai_schema( $post ) ) )
324 - ->generate_text();
323 + ->as_json_response( openstation_ai_normalize_response_schema( openstation_drafts_ai_schema( $post ) ) );
324 +
325 + $json = openstation_ai_apply_model_config(
326 + $builder,
327 + array(
328 + 'user_id' => get_current_user_id(),
329 + 'source' => 'widgets/drafts-suggestions',
330 + 'has_schema' => true,
331 + )
332 + )->generate_text();
325 333 } catch ( \Throwable $e ) {
326 334 $json = new WP_Error( 'openstation_ai_failed', $e->getMessage() );
327 335 }
328 336
329 337 if ( is_wp_error( $json ) ) {
330 - return new WP_Error(
331 - 'openstation_ai_failed',
332 - $json->get_error_message(),
333 - array( 'status' => 502 )
334 - );
338 + return openstation_drafts_ai_failure( $json );
335 339 }
336 340
337 341 $data = json_decode( (string) $json, true );
338 342 if ( ! is_array( $data ) ) {
@@ -367,8 +371,76 @@
367 371 */
368 372 $suggestions = (array) apply_filters( 'openstation_drafts_ai_suggestions', $suggestions, $post );
369 373
370 374 return new WP_REST_Response( $suggestions, 200 );
375 +}
376 +
377 +/**
378 + * Turn a failed generation into the route's error.
379 + *
380 + * The route answers 502 for every provider failure: the failure is the
381 + * upstream's, not the caller's, and a provider's own 401 or 403 passed
382 + * through as the REST status would read as "your WordPress session is
383 + * invalid" to every client on the page. What the caller needs in order to
384 + * say something useful goes into the error data instead:
385 + *
386 + * - `reason`: `quota` (out of credits or rate limited), `auth` (the site's
387 + * key was rejected), `unavailable` (the provider could not be reached or
388 + * answered 5xx) or `other`.
389 + * - `provider_status`: the provider's own HTTP status, or null when the
390 + * failure never reached the provider.
391 + * - `detail`: the provider's message, verbatim, for the console and logs.
392 + *
393 + * The top-level message says what happened in plain words. The Core AI
394 + * Client reports a rejected request as `prompt_client_error` /
395 + * `prompt_upstream_server_error` with the provider's status in
396 + * `data.status` and a message of the shape "Too Many Requests (429) -
397 + * <provider text>", which is why the provider text never reached the
398 + * widget as anything but that string.
399 + *
400 + * @param WP_Error $error Failed generation.
401 + * @return WP_Error
402 + */
403 +function openstation_drafts_ai_failure( WP_Error $error ) {
404 + $code = (string) $error->get_error_code();
405 + $data = $error->get_error_data();
406 + $detail = (string) $error->get_error_message();
407 +
408 + $provider_status = null;
409 + if ( in_array( $code, array( 'prompt_client_error', 'prompt_upstream_server_error' ), true )
410 + && is_array( $data ) && isset( $data['status'] ) ) {
411 + $provider_status = (int) $data['status'];
412 + }
413 +
414 + if ( 'prompt_network_error' === $code || ( null !== $provider_status && $provider_status >= 500 ) ) {
415 + $reason = 'unavailable';
416 + $message = __( 'The AI provider could not be reached. Try again in a moment.', 'desktop-mode' );
417 + } elseif ( in_array( $provider_status, array( 402, 429 ), true ) ) {
418 + $reason = 'quota';
419 + $message = __( 'The AI provider has no credits left or is rate limiting this site. Check its plan and billing, or try again later.', 'desktop-mode' );
420 + } elseif ( in_array( $provider_status, array( 401, 403 ), true ) ) {
421 + $reason = 'auth';
422 + $message = __( 'The AI provider rejected this site’s API key. Check the key in Settings → Connectors.', 'desktop-mode' );
423 + } elseif ( null === $provider_status && preg_match( '/quota|credits?\b|billing|rate limit/i', $detail ) ) {
424 + // A provider that failed without a status (the SDK threw instead of
425 + // answering) can still say it was the account, not the request.
426 + $reason = 'quota';
427 + $message = __( 'The AI provider has no credits left or is rate limiting this site. Check its plan and billing, or try again later.', 'desktop-mode' );
428 + } else {
429 + $reason = 'other';
430 + $message = __( 'The AI provider could not produce suggestions.', 'desktop-mode' );
431 + }
432 +
433 + return new WP_Error(
434 + 'openstation_ai_failed',
435 + $message,
436 + array(
437 + 'status' => 502,
438 + 'reason' => $reason,
439 + 'provider_status' => $provider_status,
440 + 'detail' => $detail,
441 + )
442 + );
371 443 }
372 444
373 445 /**
374 446 * Trim, tag-strip and cap a list of model-supplied strings.