| @@ -334,13 +334,9 @@ | ||
| 334 | 334 | $json = new WP_Error( 'openstation_ai_failed', $e->getMessage() ); |
| 335 | 335 | } |
| 336 | 336 | |
| 337 | 337 | if ( is_wp_error( $json ) ) { |
| 338 | - return new WP_Error( | |
| 339 | - 'openstation_ai_failed', | |
| 340 | - $json->get_error_message(), | |
| 341 | - array( 'status' => 502 ) | |
| 342 | - ); | |
| 338 | + return openstation_drafts_ai_failure( $json ); | |
| 343 | 339 | } |
| 344 | 340 | |
| 345 | 341 | $data = json_decode( (string) $json, true ); |
| 346 | 342 | if ( ! is_array( $data ) ) { |
| @@ -375,8 +371,76 @@ | ||
| 375 | 371 | */ |
| 376 | 372 | $suggestions = (array) apply_filters( 'openstation_drafts_ai_suggestions', $suggestions, $post ); |
| 377 | 373 | |
| 378 | 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 | + ); | |
| 379 | 443 | } |
| 380 | 444 | |
| 381 | 445 | /** |
| 382 | 446 | * Trim, tag-strip and cap a list of model-supplied strings. |