Skip to content

Commit 44c8a4f

Browse files
author
Mohamed Khaled
committed
Enhance ResponseException with semantic factory methods for parsing errors
1 parent beb17e1 commit 44c8a4f

File tree

8 files changed

+77
-54
lines changed

8 files changed

+77
-54
lines changed

src/Providers/Http/Exception/ResponseException.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@ public static function fromMissingData(string $apiName, string $fieldName, strin
4040
return new self($message);
4141
}
4242

43+
/**
44+
* Creates a ResponseException from invalid data in an API response.
45+
*
46+
* @since n.e.x.t
47+
*
48+
* @param string $apiName The name of the API service (e.g., 'OpenAI', 'Anthropic').
49+
* @param string $message The specific error message describing the invalid data.
50+
* @return self
51+
*/
52+
public static function fromInvalidData(string $apiName, string $message): self
53+
{
54+
return new self(sprintf('Unexpected %s API response: %s', $apiName, $message));
55+
}
4356

4457
/**
4558
* Creates a ResponseException from a bad HTTP response.

src/Providers/Http/HttpTransporter.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use WordPress\AiClient\Providers\Http\DTO\Request;
1717
use WordPress\AiClient\Providers\Http\DTO\Response;
1818
use WordPress\AiClient\Providers\Http\Exception\NetworkException;
19+
1920
/**
2021
* HTTP transporter implementation using HTTPlug.
2122
*

src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleImageGenerationModel.php

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -293,21 +293,21 @@ protected function parseResponseToGenerativeAiResult(
293293
/** @var ResponseData $responseData */
294294
$responseData = $response->getData();
295295
if (!isset($responseData['data']) || !$responseData['data']) {
296-
throw new RuntimeException(
297-
'Unexpected API response: Missing the data key.'
298-
);
296+
throw ResponseException::fromMissingData($this->providerMetadata()->getName(), 'data');
299297
}
300298
if (!is_array($responseData['data'])) {
301-
throw new RuntimeException(
302-
'Unexpected API response: The data key must contain an array.'
299+
throw ResponseException::fromInvalidData(
300+
$this->providerMetadata()->getName(),
301+
'The data key must contain an array.'
303302
);
304303
}
305304

306305
$candidates = [];
307306
foreach ($responseData['data'] as $choiceData) {
308307
if (!is_array($choiceData) || array_is_list($choiceData)) {
309-
throw new RuntimeException(
310-
'Unexpected API response: Each element in the data key must be an associative array.'
308+
throw ResponseException::fromInvalidData(
309+
$this->providerMetadata()->getName(),
310+
'Each element in the data key must be an associative array.'
311311
);
312312
}
313313

@@ -361,8 +361,10 @@ protected function parseResponseChoiceToCandidate(
361361
} elseif (isset($choiceData['b64_json']) && is_string($choiceData['b64_json'])) {
362362
$imageFile = new File($choiceData['b64_json'], $expectedMimeType);
363363
} else {
364-
throw new RuntimeException(
365-
'Unexpected API response: Each choice must contain either a url or b64_json key with a string value.'
364+
throw ResponseException::fromMissingData(
365+
$this->providerMetadata()->getName(),
366+
'url or b64_json',
367+
'choice data'
366368
);
367369
}
368370

src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModel.php

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -556,21 +556,21 @@ protected function parseResponseToGenerativeAiResult(Response $response): Genera
556556
/** @var ResponseData $responseData */
557557
$responseData = $response->getData();
558558
if (!isset($responseData['choices']) || !$responseData['choices']) {
559-
throw new RuntimeException(
560-
'Unexpected API response: Missing the choices key.'
561-
);
559+
throw ResponseException::fromMissingData($this->providerMetadata()->getName(), 'choices');
562560
}
563561
if (!is_array($responseData['choices'])) {
564-
throw new RuntimeException(
565-
'Unexpected API response: The choices key must contain an array.'
562+
throw ResponseException::fromInvalidData(
563+
$this->providerMetadata()->getName(),
564+
'The choices key must contain an array.'
566565
);
567566
}
568567

569568
$candidates = [];
570569
foreach ($responseData['choices'] as $choiceData) {
571570
if (!is_array($choiceData) || array_is_list($choiceData)) {
572-
throw new RuntimeException(
573-
'Unexpected API response: Each element in the choices key must be an associative array.'
571+
throw ResponseException::fromInvalidData(
572+
$this->providerMetadata()->getName(),
573+
'Each element in the choices key must be an associative array.'
574574
);
575575
}
576576

@@ -621,14 +621,18 @@ protected function parseResponseChoiceToCandidate(array $choiceData): Candidate
621621
!is_array($choiceData['message']) ||
622622
array_is_list($choiceData['message'])
623623
) {
624-
throw new RuntimeException(
625-
'Unexpected API response: Each choice must contain a message key with an associative array.'
624+
throw ResponseException::fromMissingData(
625+
$this->providerMetadata()->getName(),
626+
'message',
627+
'choice data'
626628
);
627629
}
628630

629631
if (!isset($choiceData['finish_reason']) || !is_string($choiceData['finish_reason'])) {
630-
throw new RuntimeException(
631-
'Unexpected API response: Each choice must contain a finish_reason key with a string value.'
632+
throw ResponseException::fromMissingData(
633+
$this->providerMetadata()->getName(),
634+
'finish_reason',
635+
'choice data'
632636
);
633637
}
634638

@@ -649,11 +653,9 @@ protected function parseResponseChoiceToCandidate(array $choiceData): Candidate
649653
$finishReason = FinishReasonEnum::toolCalls();
650654
break;
651655
default:
652-
throw new RuntimeException(
653-
sprintf(
654-
'Unexpected API response: Invalid finish reason "%s".',
655-
$choiceData['finish_reason']
656-
)
656+
throw ResponseException::fromInvalidData(
657+
$this->providerMetadata()->getName(),
658+
sprintf('Invalid finish reason "%s".', $choiceData['finish_reason'])
657659
);
658660
}
659661

@@ -703,8 +705,9 @@ protected function parseResponseChoiceMessageParts(array $messageData): array
703705
foreach ($messageData['tool_calls'] as $toolCallData) {
704706
$toolCallPart = $this->parseResponseChoiceMessageToolCallPart($toolCallData);
705707
if (!$toolCallPart) {
706-
throw new RuntimeException(
707-
'Unexpected API response: The response includes a tool call of an unexpected type.'
708+
throw ResponseException::fromInvalidData(
709+
$this->providerMetadata()->getName(),
710+
'The response includes a tool call of an unexpected type.'
708711
);
709712
}
710713
$parts[] = $toolCallPart;

tests/unit/Providers/Http/Util/ResponseUtilTest.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use PHPUnit\Framework\TestCase;
88
use WordPress\AiClient\Providers\Http\DTO\Response;
99
use WordPress\AiClient\Providers\Http\Exception\ClientException;
10-
use WordPress\AiClient\Providers\Http\Exception\ResponseException;
1110
use WordPress\AiClient\Providers\Http\Exception\ServerException;
1211
use WordPress\AiClient\Providers\Http\Util\ResponseUtil;
1312

@@ -88,7 +87,10 @@ public function testThrowIfNotSuccessfulThrowsClientExceptionFor4xxErrors(
8887

8988
$this->expectException(ClientException::class);
9089
$this->expectExceptionCode($statusCode);
91-
$this->expectExceptionMessageMatches("/^Client error \\({$statusCode}\\): Request was rejected due to client-side issue( - {$expectedMessagePart})?$/");
90+
$this->expectExceptionMessageMatches(
91+
"/^Client error \\({$statusCode}\\): Request was rejected due to " .
92+
"client-side issue( - {$expectedMessagePart})?$/"
93+
);
9294

9395
ResponseUtil::throwIfNotSuccessful($response);
9496
}
@@ -114,7 +116,9 @@ public function testThrowIfNotSuccessfulThrowsServerExceptionFor5xxErrors(
114116

115117
$this->expectException(ServerException::class);
116118
$this->expectExceptionCode($statusCode);
117-
$this->expectExceptionMessageMatches("/^Server error \\({$statusCode}\\): Request failed due to server-side issue( - {$expectedMessagePart})?$/");
119+
$this->expectExceptionMessageMatches(
120+
"/^Server error \\({$statusCode}\\): Request failed due to server-side issue( - {$expectedMessagePart})?$/"
121+
);
118122

119123
ResponseUtil::throwIfNotSuccessful($response);
120124
}

tests/unit/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleImageGenerationModelTest.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
use InvalidArgumentException;
88
use PHPUnit\Framework\TestCase;
9-
use RuntimeException;
109
use WordPress\AiClient\Files\DTO\File;
1110
use WordPress\AiClient\Files\Enums\FileTypeEnum;
1211
use WordPress\AiClient\Files\Enums\MediaOrientationEnum;
@@ -58,6 +57,7 @@ protected function setUp(): void
5857
$this->modelMetadata = $this->createStub(ModelMetadata::class);
5958
$this->modelMetadata->method('getId')->willReturn('test-image-model');
6059
$this->providerMetadata = $this->createStub(ProviderMetadata::class);
60+
$this->providerMetadata->method('getName')->willReturn('TestProvider');
6161
$this->mockHttpTransporter = $this->createMock(HttpTransporterInterface::class);
6262
$this->mockRequestAuthentication = $this->createMock(RequestAuthenticationInterface::class);
6363
}
@@ -720,8 +720,8 @@ public function testParseResponseToGenerativeAiResultMissingData(): void
720720
$response = new Response(200, [], json_encode(['id' => 'test-id']));
721721
$model = $this->createModel();
722722

723-
$this->expectException(RuntimeException::class);
724-
$this->expectExceptionMessage('Unexpected API response: Missing the data key.');
723+
$this->expectException(ResponseException::class);
724+
$this->expectExceptionMessage('Unexpected TestProvider API response: Missing the "data" key.');
725725

726726
$model->exposeParseResponseToGenerativeAiResult($response);
727727
}
@@ -740,8 +740,8 @@ public function testParseResponseToGenerativeAiResultInvalidDataType(): void
740740
);
741741
$model = $this->createModel();
742742

743-
$this->expectException(RuntimeException::class);
744-
$this->expectExceptionMessage('Unexpected API response: The data key must contain an array.');
743+
$this->expectException(ResponseException::class);
744+
$this->expectExceptionMessage('Unexpected TestProvider API response: The data key must contain an array.');
745745

746746
$model->exposeParseResponseToGenerativeAiResult($response);
747747
}
@@ -756,9 +756,9 @@ public function testParseResponseToGenerativeAiResultInvalidChoiceElementType():
756756
$response = new Response(200, [], json_encode(['data' => ['invalid']]));
757757
$model = $this->createModel();
758758

759-
$this->expectException(RuntimeException::class);
759+
$this->expectException(ResponseException::class);
760760
$this->expectExceptionMessage(
761-
'Unexpected API response: Each element in the data key must be an associative array.'
761+
'Unexpected TestProvider API response: Each element in the data key must be an associative array.'
762762
);
763763

764764
$model->exposeParseResponseToGenerativeAiResult($response);
@@ -820,9 +820,9 @@ public function testParseResponseChoiceToCandidateMissingUrlOrB64Json(): void
820820
];
821821
$model = $this->createModel();
822822

823-
$this->expectException(RuntimeException::class);
823+
$this->expectException(ResponseException::class);
824824
$this->expectExceptionMessage(
825-
'Unexpected API response: Each choice must contain either a url or b64_json key with a string value.'
825+
'Unexpected TestProvider API response: Missing the "url or b64_json" key in choice data.'
826826
);
827827

828828
$model->exposeParseResponseChoiceToCandidate($choiceData);

tests/unit/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleModelMetadataDirectoryTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
use WordPress\AiClient\Providers\Http\Contracts\RequestAuthenticationInterface;
1010
use WordPress\AiClient\Providers\Http\DTO\Response;
1111
use WordPress\AiClient\Providers\Http\Exception\ClientException;
12-
use WordPress\AiClient\Providers\Http\Exception\ResponseException;
1312
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
1413

1514
/**

tests/unit/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModelTest.php

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ protected function setUp(): void
6060
$this->modelMetadata = $this->createStub(ModelMetadata::class);
6161
$this->modelMetadata->method('getId')->willReturn('test-model');
6262
$this->providerMetadata = $this->createStub(ProviderMetadata::class);
63+
$this->providerMetadata->method('getName')->willReturn('TestProvider');
6364
$this->mockHttpTransporter = $this->createMock(HttpTransporterInterface::class);
6465
$this->mockRequestAuthentication = $this->createMock(RequestAuthenticationInterface::class);
6566
}
@@ -950,8 +951,8 @@ public function testParseResponseToGenerativeAiResultMissingChoices(): void
950951
$response = new Response(200, [], json_encode(['id' => 'test-id']));
951952
$model = $this->createModel();
952953

953-
$this->expectException(RuntimeException::class);
954-
$this->expectExceptionMessage('Unexpected API response: Missing the choices key.');
954+
$this->expectException(ResponseException::class);
955+
$this->expectExceptionMessage('Unexpected TestProvider API response: Missing the "choices" key.');
955956

956957
$model->parseResponseToGenerativeAiResult($response);
957958
}
@@ -970,8 +971,8 @@ public function testParseResponseToGenerativeAiResultInvalidChoicesType(): void
970971
);
971972
$model = $this->createModel();
972973

973-
$this->expectException(RuntimeException::class);
974-
$this->expectExceptionMessage('Unexpected API response: The choices key must contain an array.');
974+
$this->expectException(ResponseException::class);
975+
$this->expectExceptionMessage('Unexpected TestProvider API response: The choices key must contain an array.');
975976

976977
$model->parseResponseToGenerativeAiResult($response);
977978
}
@@ -986,9 +987,9 @@ public function testParseResponseToGenerativeAiResultInvalidChoiceElementType():
986987
$response = new Response(200, [], json_encode(['choices' => ['invalid']]));
987988
$model = $this->createModel();
988989

989-
$this->expectException(RuntimeException::class);
990+
$this->expectException(ResponseException::class);
990991
$this->expectExceptionMessage(
991-
'Unexpected API response: Each element in the choices key must be an associative array.'
992+
'Unexpected TestProvider API response: Each element in the choices key must be an associative array.'
992993
);
993994

994995
$model->parseResponseToGenerativeAiResult($response);
@@ -1028,9 +1029,9 @@ public function testParseResponseChoiceToCandidateMissingMessage(): void
10281029
];
10291030
$model = $this->createModel();
10301031

1031-
$this->expectException(RuntimeException::class);
1032+
$this->expectException(ResponseException::class);
10321033
$this->expectExceptionMessage(
1033-
'Unexpected API response: Each choice must contain a message key with an associative array.'
1034+
'Unexpected TestProvider API response: Missing the "message" key in choice data.'
10341035
);
10351036

10361037
$model->exposeParseResponseChoiceToCandidate($choiceData);
@@ -1049,9 +1050,9 @@ public function testParseResponseChoiceToCandidateInvalidMessageType(): void
10491050
];
10501051
$model = $this->createModel();
10511052

1052-
$this->expectException(RuntimeException::class);
1053+
$this->expectException(ResponseException::class);
10531054
$this->expectExceptionMessage(
1054-
'Unexpected API response: Each choice must contain a message key with an associative array.'
1055+
'Unexpected TestProvider API response: Missing the "message" key in choice data.'
10551056
);
10561057

10571058
$model->exposeParseResponseChoiceToCandidate($choiceData);
@@ -1072,9 +1073,9 @@ public function testParseResponseChoiceToCandidateMissingFinishReason(): void
10721073
];
10731074
$model = $this->createModel();
10741075

1075-
$this->expectException(RuntimeException::class);
1076+
$this->expectException(ResponseException::class);
10761077
$this->expectExceptionMessage(
1077-
'Unexpected API response: Each choice must contain a finish_reason key with a string value.'
1078+
'Unexpected TestProvider API response: Missing the "finish_reason" key in choice data.'
10781079
);
10791080

10801081
$model->exposeParseResponseChoiceToCandidate($choiceData);
@@ -1096,8 +1097,8 @@ public function testParseResponseChoiceToCandidateInvalidFinishReason(): void
10961097
];
10971098
$model = $this->createModel();
10981099

1099-
$this->expectException(RuntimeException::class);
1100-
$this->expectExceptionMessage('Unexpected API response: Invalid finish reason "unknown".');
1100+
$this->expectException(ResponseException::class);
1101+
$this->expectExceptionMessage('Unexpected TestProvider API response: Invalid finish reason "unknown".');
11011102

11021103
$model->exposeParseResponseChoiceToCandidate($choiceData);
11031104
}

0 commit comments

Comments
 (0)