Changeset 62037
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php
r61942 r62037 9 9 10 10 use WordPress\AiClient\Builders\PromptBuilder; 11 use WordPress\AiClient\Common\Exception\InvalidArgumentException; 12 use WordPress\AiClient\Common\Exception\TokenLimitReachedException; 11 13 use WordPress\AiClient\Files\DTO\File; 12 14 use WordPress\AiClient\Files\Enums\FileTypeEnum; … … 16 18 use WordPress\AiClient\Messages\Enums\ModalityEnum; 17 19 use WordPress\AiClient\Providers\Http\DTO\RequestOptions; 20 use WordPress\AiClient\Providers\Http\Exception\ClientException; 21 use WordPress\AiClient\Providers\Http\Exception\NetworkException; 22 use WordPress\AiClient\Providers\Http\Exception\ServerException; 18 23 use WordPress\AiClient\Providers\Models\Contracts\ModelInterface; 19 24 use WordPress\AiClient\Providers\Models\DTO\ModelConfig; … … 180 185 } catch ( Exception $e ) { 181 186 $this->builder = new PromptBuilder( $registry ); 182 $this->error = new WP_Error( 183 'prompt_builder_error', 184 $e->getMessage(), 185 array( 186 'exception_class' => get_class( $e ), 187 ) 188 ); 187 $this->error = $this->exception_to_wp_error( $e ); 189 188 } 190 189 … … 312 311 __( 'Prompt execution was prevented by a filter.' ), 313 312 array( 314 ' exception_class' => 'WP_AI_Client_Prompt_Prevented',313 'status' => 503, 315 314 ) 316 315 ); … … 334 333 return $result; 335 334 } catch ( Exception $e ) { 336 $this->error = new WP_Error( 337 'prompt_builder_error', 338 $e->getMessage(), 339 array( 340 'exception_class' => get_class( $e ), 341 ) 342 ); 335 $this->error = $this->exception_to_wp_error( $e ); 343 336 344 337 if ( self::is_generating_method( $name ) ) { … … 347 340 return $this; 348 341 } 342 } 343 344 /** 345 * Converts an exception into a WP_Error with a structured error code and message. 346 * 347 * This method maps different exception types to specific WP_Error codes and HTTP status codes. 348 * The presence of the status codes means these WP_Error objects can be easily used in REST API responses 349 * or other contexts where HTTP semantics are relevant. 350 * 351 * @since 7.0.0 352 * 353 * @param Exception $e The exception to convert. 354 * @return WP_Error The resulting WP_Error object. 355 */ 356 private function exception_to_wp_error( Exception $e ): WP_Error { 357 if ( $e instanceof NetworkException ) { 358 $error_code = 'prompt_network_error'; 359 $status_code = 503; 360 } elseif ( $e instanceof ClientException ) { 361 // `ClientException` uses HTTP status codes as exception codes, so we can rely on them. 362 $error_code = 'prompt_client_error'; 363 $status_code = $e->getCode() ? $e->getCode() : 400; 364 } elseif ( $e instanceof ServerException ) { 365 // `ServerException` uses HTTP status codes as exception codes, so we can rely on them. 366 $error_code = 'prompt_upstream_server_error'; 367 $status_code = $e->getCode() ? $e->getCode() : 500; 368 } elseif ( $e instanceof TokenLimitReachedException ) { 369 $error_code = 'prompt_token_limit_reached'; 370 $status_code = 400; 371 } elseif ( $e instanceof InvalidArgumentException ) { 372 $error_code = 'prompt_invalid_argument'; 373 $status_code = 400; 374 } else { 375 $error_code = 'prompt_builder_error'; 376 $status_code = 500; 377 } 378 379 return new WP_Error( 380 $error_code, 381 $e->getMessage(), 382 array( 383 'status' => $status_code, 384 'exception_class' => get_class( $e ), 385 ) 386 ); 349 387 } 350 388 -
trunk/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php
r61942 r62037 33 33 use WordPress\AiClient\Results\Enums\FinishReasonEnum; 34 34 use WordPress\AiClient\Builders\PromptBuilder; 35 use WordPress\AiClient\Common\Exception\InvalidArgumentException as AiClientInvalidArgumentException; 36 use WordPress\AiClient\Common\Exception\TokenLimitReachedException; 37 use WordPress\AiClient\Providers\Http\Exception\ClientException; 38 use WordPress\AiClient\Providers\Http\Exception\NetworkException; 39 use WordPress\AiClient\Providers\Http\Exception\ServerException; 35 40 use WordPress\AiClient\Tools\DTO\FunctionDeclaration; 36 41 use WordPress\AiClient\Tools\DTO\FunctionResponse; … … 903 908 904 909 $this->assertWPError( $result ); 905 $this->assertSame( 'prompt_ builder_error', $result->get_error_code() );910 $this->assertSame( 'prompt_invalid_argument', $result->get_error_code() ); 906 911 $this->assertStringContainsString( 907 912 'Model preferences must be model identifiers', … … 927 932 928 933 $this->assertWPError( $result ); 929 $this->assertSame( 'prompt_ builder_error', $result->get_error_code() );934 $this->assertSame( 'prompt_invalid_argument', $result->get_error_code() ); 930 935 $this->assertStringContainsString( 931 936 'Model preference tuple must contain model identifier and provider ID.', … … 946 951 947 952 $this->assertWPError( $result ); 948 $this->assertSame( 'prompt_ builder_error', $result->get_error_code() );953 $this->assertSame( 'prompt_invalid_argument', $result->get_error_code() ); 949 954 $this->assertStringContainsString( 950 955 'Model preference identifiers cannot be empty.', … … 965 970 966 971 $this->assertWPError( $result ); 967 $this->assertSame( 'prompt_ builder_error', $result->get_error_code() );972 $this->assertSame( 'prompt_invalid_argument', $result->get_error_code() ); 968 973 $this->assertStringContainsString( 969 974 'At least one model preference must be provided.', … … 1287 1292 1288 1293 $this->assertWPError( $result ); 1289 $this->assertSame( 'prompt_ builder_error', $result->get_error_code() );1294 $this->assertSame( 'prompt_invalid_argument', $result->get_error_code() ); 1290 1295 $this->assertStringContainsString( 'Cannot generate from an empty prompt', $result->get_error_message() ); 1291 1296 } … … 1308 1313 1309 1314 $this->assertWPError( $result ); 1310 $this->assertSame( 'prompt_ builder_error', $result->get_error_code() );1315 $this->assertSame( 'prompt_invalid_argument', $result->get_error_code() ); 1311 1316 $this->assertStringContainsString( 'The first message must be from a user role', $result->get_error_message() ); 1312 1317 } … … 1341 1346 1342 1347 $this->assertWPError( $result ); 1343 $this->assertSame( 'prompt_ builder_error', $result->get_error_code() );1348 $this->assertSame( 'prompt_invalid_argument', $result->get_error_code() ); 1344 1349 $this->assertStringContainsString( 'The last message must be from a user role', $result->get_error_message() ); 1345 1350 } … … 1355 1360 1356 1361 $this->assertWPError( $result ); 1357 $this->assertSame( 'prompt_ builder_error', $result->get_error_code() );1362 $this->assertSame( 'prompt_invalid_argument', $result->get_error_code() ); 1358 1363 $this->assertStringContainsString( 'Cannot create a message from an empty string', $result->get_error_message() ); 1359 1364 } … … 1369 1374 1370 1375 $this->assertWPError( $result ); 1371 $this->assertSame( 'prompt_ builder_error', $result->get_error_code() );1376 $this->assertSame( 'prompt_invalid_argument', $result->get_error_code() ); 1372 1377 $this->assertStringContainsString( 'Cannot create a message from an empty array', $result->get_error_message() ); 1373 1378 } … … 1383 1388 1384 1389 $this->assertWPError( $result ); 1385 $this->assertSame( 'prompt_ builder_error', $result->get_error_code() );1390 $this->assertSame( 'prompt_invalid_argument', $result->get_error_code() ); 1386 1391 $this->assertStringContainsString( 'Input must be a string, MessagePart, MessagePartArrayShape', $result->get_error_message() ); 1387 1392 } … … 1403 1408 1404 1409 $this->assertWPError( $result ); 1405 $this->assertSame( 'prompt_ builder_error', $result->get_error_code() );1410 $this->assertSame( 'prompt_invalid_argument', $result->get_error_code() ); 1406 1411 } 1407 1412 … … 2523 2528 2524 2529 $this->assertWPError( $error, 'generate_text should return WP_Error when exception occurs' ); 2525 $this->assertSame( 'prompt_ builder_error', $error->get_error_code() );2530 $this->assertSame( 'prompt_invalid_argument', $error->get_error_code() ); 2526 2531 2527 2532 $error_data = $error->get_error_data(); … … 2598 2603 2599 2604 $this->assertWPError( $result, 'generate_text should return WP_Error when exception occurs' ); 2600 $this->assertSame( 'prompt_ builder_error', $result->get_error_code() );2605 $this->assertSame( 'prompt_invalid_argument', $result->get_error_code() ); 2601 2606 $this->assertSame( 'Model preference tuple must contain model identifier and provider ID.', $result->get_error_message() ); 2602 2607 … … 2606 2611 $this->assertNotEmpty( $error_data['exception_class'] ); 2607 2612 } 2613 2614 /** 2615 * Invokes the private exception_to_wp_error method via reflection. 2616 * 2617 * @param WP_AI_Client_Prompt_Builder $builder The builder instance. 2618 * @param Exception $exception The exception to convert. 2619 * @return WP_Error The resulting WP_Error. 2620 */ 2621 private function invoke_exception_to_wp_error( WP_AI_Client_Prompt_Builder $builder, Exception $exception ): WP_Error { 2622 $reflection = new ReflectionClass( WP_AI_Client_Prompt_Builder::class ); 2623 $method = $reflection->getMethod( 'exception_to_wp_error' ); 2624 self::set_accessible( $method ); 2625 2626 return $method->invoke( $builder, $exception ); 2627 } 2628 2629 /** 2630 * Tests exception_to_wp_error maps NetworkException correctly. 2631 * 2632 * @ticket 64591 2633 */ 2634 public function test_exception_to_wp_error_network_exception() { 2635 $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() ); 2636 $error = $this->invoke_exception_to_wp_error( 2637 $builder, 2638 new NetworkException( 'Connection timed out' ) 2639 ); 2640 2641 $this->assertSame( 'prompt_network_error', $error->get_error_code() ); 2642 $this->assertSame( 'Connection timed out', $error->get_error_message() ); 2643 $this->assertSame( 503, $error->get_error_data()['status'] ); 2644 $this->assertSame( NetworkException::class, $error->get_error_data()['exception_class'] ); 2645 } 2646 2647 /** 2648 * Tests exception_to_wp_error maps ClientException with a custom code. 2649 * 2650 * @ticket 64591 2651 */ 2652 public function test_exception_to_wp_error_client_exception_with_code() { 2653 $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() ); 2654 $error = $this->invoke_exception_to_wp_error( 2655 $builder, 2656 new ClientException( 'Unauthorized', 401 ) 2657 ); 2658 2659 $this->assertSame( 'prompt_client_error', $error->get_error_code() ); 2660 $this->assertSame( 'Unauthorized', $error->get_error_message() ); 2661 $this->assertSame( 401, $error->get_error_data()['status'] ); 2662 $this->assertSame( ClientException::class, $error->get_error_data()['exception_class'] ); 2663 } 2664 2665 /** 2666 * Tests exception_to_wp_error maps ClientException without a code to 400. 2667 * 2668 * @ticket 64591 2669 */ 2670 public function test_exception_to_wp_error_client_exception_without_code() { 2671 $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() ); 2672 $error = $this->invoke_exception_to_wp_error( 2673 $builder, 2674 new ClientException( 'Bad request' ) 2675 ); 2676 2677 $this->assertSame( 'prompt_client_error', $error->get_error_code() ); 2678 $this->assertSame( 'Bad request', $error->get_error_message() ); 2679 $this->assertSame( 400, $error->get_error_data()['status'] ); 2680 } 2681 2682 /** 2683 * Tests exception_to_wp_error maps ServerException with a custom code. 2684 * 2685 * @ticket 64591 2686 */ 2687 public function test_exception_to_wp_error_server_exception_with_code() { 2688 $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() ); 2689 $error = $this->invoke_exception_to_wp_error( 2690 $builder, 2691 new ServerException( 'Bad gateway', 502 ) 2692 ); 2693 2694 $this->assertSame( 'prompt_upstream_server_error', $error->get_error_code() ); 2695 $this->assertSame( 'Bad gateway', $error->get_error_message() ); 2696 $this->assertSame( 502, $error->get_error_data()['status'] ); 2697 $this->assertSame( ServerException::class, $error->get_error_data()['exception_class'] ); 2698 } 2699 2700 /** 2701 * Tests exception_to_wp_error maps ServerException without a code to 500. 2702 * 2703 * @ticket 64591 2704 */ 2705 public function test_exception_to_wp_error_server_exception_without_code() { 2706 $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() ); 2707 $error = $this->invoke_exception_to_wp_error( 2708 $builder, 2709 new ServerException( 'Internal server error' ) 2710 ); 2711 2712 $this->assertSame( 'prompt_upstream_server_error', $error->get_error_code() ); 2713 $this->assertSame( 'Internal server error', $error->get_error_message() ); 2714 $this->assertSame( 500, $error->get_error_data()['status'] ); 2715 } 2716 2717 /** 2718 * Tests exception_to_wp_error maps TokenLimitReachedException correctly. 2719 * 2720 * @ticket 64591 2721 */ 2722 public function test_exception_to_wp_error_token_limit_reached_exception() { 2723 $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() ); 2724 $error = $this->invoke_exception_to_wp_error( 2725 $builder, 2726 new TokenLimitReachedException( 'Token limit exceeded', 4096 ) 2727 ); 2728 2729 $this->assertSame( 'prompt_token_limit_reached', $error->get_error_code() ); 2730 $this->assertSame( 'Token limit exceeded', $error->get_error_message() ); 2731 $this->assertSame( 400, $error->get_error_data()['status'] ); 2732 $this->assertSame( TokenLimitReachedException::class, $error->get_error_data()['exception_class'] ); 2733 } 2734 2735 /** 2736 * Tests exception_to_wp_error maps InvalidArgumentException correctly. 2737 * 2738 * @ticket 64591 2739 */ 2740 public function test_exception_to_wp_error_invalid_argument_exception() { 2741 $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() ); 2742 $error = $this->invoke_exception_to_wp_error( 2743 $builder, 2744 new AiClientInvalidArgumentException( 'Invalid model parameter' ) 2745 ); 2746 2747 $this->assertSame( 'prompt_invalid_argument', $error->get_error_code() ); 2748 $this->assertSame( 'Invalid model parameter', $error->get_error_message() ); 2749 $this->assertSame( 400, $error->get_error_data()['status'] ); 2750 $this->assertSame( AiClientInvalidArgumentException::class, $error->get_error_data()['exception_class'] ); 2751 } 2752 2753 /** 2754 * Tests exception_to_wp_error maps a generic Exception to the fallback error. 2755 * 2756 * @ticket 64591 2757 */ 2758 public function test_exception_to_wp_error_generic_exception() { 2759 $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() ); 2760 $error = $this->invoke_exception_to_wp_error( 2761 $builder, 2762 new Exception( 'Something went wrong' ) 2763 ); 2764 2765 $this->assertSame( 'prompt_builder_error', $error->get_error_code() ); 2766 $this->assertSame( 'Something went wrong', $error->get_error_message() ); 2767 $this->assertSame( 500, $error->get_error_data()['status'] ); 2768 $this->assertSame( 'Exception', $error->get_error_data()['exception_class'] ); 2769 } 2770 2771 /** 2772 * Tests exception_to_wp_error always includes status and exception_class in error data. 2773 * 2774 * @ticket 64591 2775 * 2776 * @dataProvider data_exception_to_wp_error_error_data_structure 2777 * 2778 * @param Exception $exception The exception to convert. 2779 */ 2780 public function test_exception_to_wp_error_error_data_structure( Exception $exception ) { 2781 $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() ); 2782 $error = $this->invoke_exception_to_wp_error( $builder, $exception ); 2783 2784 $data = $error->get_error_data(); 2785 $this->assertIsArray( $data ); 2786 $this->assertArrayHasKey( 'status', $data ); 2787 $this->assertIsInt( $data['status'] ); 2788 $this->assertArrayHasKey( 'exception_class', $data ); 2789 $this->assertIsString( $data['exception_class'] ); 2790 } 2791 2792 /** 2793 * Data provider for test_exception_to_wp_error_error_data_structure. 2794 * 2795 * @return array<string, array{0: Exception}> 2796 */ 2797 public static function data_exception_to_wp_error_error_data_structure(): array { 2798 return array( 2799 'NetworkException' => array( new NetworkException( 'network error' ) ), 2800 'ClientException' => array( new ClientException( 'client error', 422 ) ), 2801 'ServerException' => array( new ServerException( 'server error', 503 ) ), 2802 'TokenLimitReachedException' => array( new TokenLimitReachedException( 'token limit' ) ), 2803 'InvalidArgumentException' => array( new AiClientInvalidArgumentException( 'invalid arg' ) ), 2804 'generic Exception' => array( new Exception( 'generic' ) ), 2805 ); 2806 } 2608 2807 }
Note: See TracChangeset
for help on using the changeset viewer.