Skip to content

Commit 983d66e

Browse files
Merge pull request #102 from WordPress/enhance/response-exception-invalid-value
Enhance exception API for response exceptions from invalid values by including concrete field name
2 parents 5220773 + 91a9d42 commit 983d66e

7 files changed

+56
-33
lines changed

src/Providers/Http/Exception/ResponseException.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,12 @@ public static function fromMissingData(string $apiName, string $fieldName): self
3939
* @since n.e.x.t
4040
*
4141
* @param string $apiName The name of the API service (e.g., 'OpenAI', 'Anthropic').
42+
* @param string $fieldName The field that was invalid.
4243
* @param string $message The specific error message describing the invalid data.
4344
* @return self
4445
*/
45-
public static function fromInvalidData(string $apiName, string $message): self
46+
public static function fromInvalidData(string $apiName, string $fieldName, string $message): self
4647
{
47-
return new self(sprintf('Unexpected %s API response: %s', $apiName, $message));
48+
return new self(sprintf('Unexpected %s API response: Invalid "%s" key: %s', $apiName, $fieldName, $message));
4849
}
4950
}

src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleImageGenerationModel.php

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -302,20 +302,22 @@ protected function parseResponseToGenerativeAiResult(
302302
if (!is_array($responseData['data'])) {
303303
throw ResponseException::fromInvalidData(
304304
$this->providerMetadata()->getName(),
305-
'The data key must contain an array.'
305+
'data',
306+
'The value must be an array.'
306307
);
307308
}
308309

309310
$candidates = [];
310-
foreach ($responseData['data'] as $choiceData) {
311+
foreach ($responseData['data'] as $index => $choiceData) {
311312
if (!is_array($choiceData) || array_is_list($choiceData)) {
312313
throw ResponseException::fromInvalidData(
313314
$this->providerMetadata()->getName(),
314-
'Each element in the data key must be an associative array.'
315+
"data[{$index}]",
316+
'The value must be an associative array.'
315317
);
316318
}
317319

318-
$candidates[] = $this->parseResponseChoiceToCandidate($choiceData, $expectedMimeType);
320+
$candidates[] = $this->parseResponseChoiceToCandidate($choiceData, $index, $expectedMimeType);
319321
}
320322

321323
$id = isset($responseData['id']) && is_string($responseData['id']) ? $responseData['id'] : '';
@@ -352,12 +354,14 @@ protected function parseResponseToGenerativeAiResult(
352354
* @since 0.1.0
353355
*
354356
* @param ChoiceData $choiceData The choice data from the API response.
357+
* @param int $index The index of the choice in the choices array.
355358
* @param string $expectedMimeType The expected MIME type the response is in.
356359
* @return Candidate The parsed candidate.
357360
* @throws RuntimeException If the choice data is invalid.
358361
*/
359362
protected function parseResponseChoiceToCandidate(
360363
array $choiceData,
364+
int $index,
361365
string $expectedMimeType = 'image/png'
362366
): Candidate {
363367
if (isset($choiceData['url']) && is_string($choiceData['url'])) {
@@ -367,7 +371,8 @@ protected function parseResponseChoiceToCandidate(
367371
} else {
368372
throw ResponseException::fromInvalidData(
369373
$this->providerMetadata()->getName(),
370-
'Each choice must contain either a url or b64_json key with a string value.'
374+
"choices[{$index}]",
375+
'The value must contain either a url or b64_json key with a string value.'
371376
);
372377
}
373378

src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModel.php

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,8 @@ protected function parseResponseToGenerativeAiResult(Response $response): Genera
565565
if (!is_array($responseData['choices'])) {
566566
throw ResponseException::fromInvalidData(
567567
$this->providerMetadata()->getName(),
568-
'The choices key must contain an array.'
568+
'choices',
569+
'The value must be an array.'
569570
);
570571
}
571572

@@ -574,7 +575,8 @@ protected function parseResponseToGenerativeAiResult(Response $response): Genera
574575
if (!is_array($choiceData) || array_is_list($choiceData)) {
575576
throw ResponseException::fromInvalidData(
576577
$this->providerMetadata()->getName(),
577-
'Each element in the choices key must be an associative array.'
578+
"choices[{$index}]",
579+
'The value must be an associative array.'
578580
);
579581
}
580582

@@ -640,7 +642,7 @@ protected function parseResponseChoiceToCandidate(array $choiceData, int $index)
640642
}
641643

642644
$messageData = $choiceData['message'];
643-
$message = $this->parseResponseChoiceMessage($messageData);
645+
$message = $this->parseResponseChoiceMessage($messageData, $index);
644646

645647
switch ($choiceData['finish_reason']) {
646648
case 'stop':
@@ -658,6 +660,7 @@ protected function parseResponseChoiceToCandidate(array $choiceData, int $index)
658660
default:
659661
throw ResponseException::fromInvalidData(
660662
$this->providerMetadata()->getName(),
663+
"choices[{$index}].finish_reason",
661664
sprintf('Invalid finish reason "%s".', $choiceData['finish_reason'])
662665
);
663666
}
@@ -671,15 +674,16 @@ protected function parseResponseChoiceToCandidate(array $choiceData, int $index)
671674
* @since 0.1.0
672675
*
673676
* @param MessageData $messageData The message data from the API response.
677+
* @param int $index The index of the choice in the choices array.
674678
* @return Message The parsed message.
675679
*/
676-
protected function parseResponseChoiceMessage(array $messageData): Message
680+
protected function parseResponseChoiceMessage(array $messageData, int $index): Message
677681
{
678682
$role = isset($messageData['role']) && 'user' === $messageData['role']
679683
? MessageRoleEnum::user()
680684
: MessageRoleEnum::model();
681685

682-
$parts = $this->parseResponseChoiceMessageParts($messageData);
686+
$parts = $this->parseResponseChoiceMessageParts($messageData, $index);
683687

684688
return new Message($role, $parts);
685689
}
@@ -690,9 +694,10 @@ protected function parseResponseChoiceMessage(array $messageData): Message
690694
* @since 0.1.0
691695
*
692696
* @param MessageData $messageData The message data from the API response.
697+
* @param int $index The index of the choice in the choices array.
693698
* @return MessagePart[] The parsed message parts.
694699
*/
695-
protected function parseResponseChoiceMessageParts(array $messageData): array
700+
protected function parseResponseChoiceMessageParts(array $messageData, int $index): array
696701
{
697702
$parts = [];
698703

@@ -705,11 +710,12 @@ protected function parseResponseChoiceMessageParts(array $messageData): array
705710
}
706711

707712
if (isset($messageData['tool_calls']) && is_array($messageData['tool_calls'])) {
708-
foreach ($messageData['tool_calls'] as $toolCallData) {
713+
foreach ($messageData['tool_calls'] as $toolCallIndex => $toolCallData) {
709714
$toolCallPart = $this->parseResponseChoiceMessageToolCallPart($toolCallData);
710715
if (!$toolCallPart) {
711716
throw ResponseException::fromInvalidData(
712717
$this->providerMetadata()->getName(),
718+
"choices[{$index}].message.tool_calls[{$toolCallIndex}]",
713719
'The response includes a tool call of an unexpected type.'
714720
);
715721
}

tests/mocks/MockOpenAiCompatibleImageGenerationModel.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,15 @@ public function exposeParseResponseToGenerativeAiResult(
9191
* Exposes the protected parseResponseChoiceToCandidate method.
9292
*
9393
* @param array<string, mixed> $choiceData
94+
* @param int $index
9495
* @param string $expectedMimeType
9596
* @return \WordPress\AiClient\Results\DTO\Candidate
9697
*/
9798
public function exposeParseResponseChoiceToCandidate(
9899
array $choiceData,
100+
int $index,
99101
string $expectedMimeType = 'image/png'
100102
): \WordPress\AiClient\Results\DTO\Candidate {
101-
return $this->parseResponseChoiceToCandidate($choiceData, $expectedMimeType);
103+
return $this->parseResponseChoiceToCandidate($choiceData, $index, $expectedMimeType);
102104
}
103105
}

tests/unit/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleImageGenerationModelTest.php

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -741,7 +741,9 @@ public function testParseResponseToGenerativeAiResultInvalidDataType(): void
741741
$model = $this->createModel();
742742

743743
$this->expectException(ResponseException::class);
744-
$this->expectExceptionMessage('Unexpected TestProvider API response: The data key must contain an array.');
744+
$this->expectExceptionMessage(
745+
'Unexpected TestProvider API response: Invalid "data" key: The value must be an array.'
746+
);
745747

746748
$model->exposeParseResponseToGenerativeAiResult($response);
747749
}
@@ -758,7 +760,7 @@ public function testParseResponseToGenerativeAiResultInvalidChoiceElementType():
758760

759761
$this->expectException(ResponseException::class);
760762
$this->expectExceptionMessage(
761-
'Unexpected TestProvider API response: Each element in the data key must be an associative array.'
763+
'Unexpected TestProvider API response: Invalid "data[0]" key: The value must be an associative array.'
762764
);
763765

764766
$model->exposeParseResponseToGenerativeAiResult($response);
@@ -775,7 +777,7 @@ public function testParseResponseChoiceToCandidateValidUrlData(): void
775777
'url' => 'https://example.com/image.png',
776778
];
777779
$model = $this->createModel();
778-
$candidate = $model->exposeParseResponseChoiceToCandidate($choiceData, 'image/png');
780+
$candidate = $model->exposeParseResponseChoiceToCandidate($choiceData, 0, 'image/png');
779781

780782
$this->assertInstanceOf(Candidate::class, $candidate);
781783
$this->assertEquals(
@@ -799,7 +801,7 @@ public function testParseResponseChoiceToCandidateValidB64JsonData(): void
799801
'b64_json' => $base64Image,
800802
];
801803
$model = $this->createModel();
802-
$candidate = $model->exposeParseResponseChoiceToCandidate($choiceData, 'image/png');
804+
$candidate = $model->exposeParseResponseChoiceToCandidate($choiceData, 0, 'image/png');
803805

804806
$this->assertInstanceOf(Candidate::class, $candidate);
805807
$this->assertEquals($base64Image, $candidate->getMessage()->getParts()[0]->getFile()->getBase64Data());
@@ -822,10 +824,10 @@ public function testParseResponseChoiceToCandidateMissingUrlOrB64Json(): void
822824

823825
$this->expectException(ResponseException::class);
824826
$this->expectExceptionMessage(
825-
'Unexpected TestProvider API response: Each choice must contain either a url or b64_json key with a ' .
826-
'string value.'
827+
'Unexpected TestProvider API response: Invalid "choices[0]" key: The value must contain either a ' .
828+
'url or b64_json key with a string value.'
827829
);
828830

829-
$model->exposeParseResponseChoiceToCandidate($choiceData);
831+
$model->exposeParseResponseChoiceToCandidate($choiceData, 0);
830832
}
831833
}

tests/unit/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModelTest.php

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -972,7 +972,9 @@ public function testParseResponseToGenerativeAiResultInvalidChoicesType(): void
972972
$model = $this->createModel();
973973

974974
$this->expectException(ResponseException::class);
975-
$this->expectExceptionMessage('Unexpected TestProvider API response: The choices key must contain an array.');
975+
$this->expectExceptionMessage(
976+
'Unexpected TestProvider API response: Invalid "choices" key: The value must be an array.'
977+
);
976978

977979
$model->parseResponseToGenerativeAiResult($response);
978980
}
@@ -989,7 +991,7 @@ public function testParseResponseToGenerativeAiResultInvalidChoiceElementType():
989991

990992
$this->expectException(ResponseException::class);
991993
$this->expectExceptionMessage(
992-
'Unexpected TestProvider API response: Each element in the choices key must be an associative array.'
994+
'Unexpected TestProvider API response: Invalid "choices[0]" key: The value must be an associative array.'
993995
);
994996

995997
$model->parseResponseToGenerativeAiResult($response);
@@ -1098,7 +1100,12 @@ public function testParseResponseChoiceToCandidateInvalidFinishReason(): void
10981100
$model = $this->createModel();
10991101

11001102
$this->expectException(ResponseException::class);
1101-
$this->expectExceptionMessage('Unexpected TestProvider API response: Invalid finish reason "unknown".');
1103+
$this->expectExceptionMessage(
1104+
sprintf(
1105+
'Unexpected TestProvider API response: Invalid "%s" key: Invalid finish reason "unknown".',
1106+
'choices[0].finish_reason'
1107+
)
1108+
);
11021109

11031110
$model->exposeParseResponseChoiceToCandidate($choiceData);
11041111
}
@@ -1115,7 +1122,7 @@ public function testParseResponseChoiceMessageAssistant(): void
11151122
'content' => 'Assistant response',
11161123
];
11171124
$model = $this->createModel();
1118-
$message = $model->exposeParseResponseChoiceMessage($messageData);
1125+
$message = $model->exposeParseResponseChoiceMessage($messageData, 0);
11191126

11201127
$this->assertEquals(MessageRoleEnum::model(), $message->getRole());
11211128
$this->assertCount(1, $message->getParts());
@@ -1134,7 +1141,7 @@ public function testParseResponseChoiceMessageUser(): void
11341141
'content' => 'User response',
11351142
];
11361143
$model = $this->createModel();
1137-
$message = $model->exposeParseResponseChoiceMessage($messageData);
1144+
$message = $model->exposeParseResponseChoiceMessage($messageData, 0);
11381145

11391146
$this->assertEquals(MessageRoleEnum::user(), $message->getRole());
11401147
$this->assertCount(1, $message->getParts());
@@ -1153,7 +1160,7 @@ public function testParseResponseChoiceMessagePartsContentAndReasoning(): void
11531160
'content' => 'Final answer',
11541161
];
11551162
$model = $this->createModel();
1156-
$parts = $model->exposeParseResponseChoiceMessageParts($messageData);
1163+
$parts = $model->exposeParseResponseChoiceMessageParts($messageData, 0);
11571164

11581165
$this->assertCount(2, $parts);
11591166
$this->assertEquals('Thinking process', $parts[0]->getText());
@@ -1182,7 +1189,7 @@ public function testParseResponseChoiceMessagePartsToolCalls(): void
11821189
],
11831190
];
11841191
$model = $this->createModel();
1185-
$parts = $model->exposeParseResponseChoiceMessageParts($messageData);
1192+
$parts = $model->exposeParseResponseChoiceMessageParts($messageData, 0);
11861193

11871194
$this->assertCount(1, $parts);
11881195
$this->assertInstanceOf(FunctionCall::class, $parts[0]->getFunctionCall());

tests/unit/Providers/OpenAiCompatibleImplementation/MockOpenAiCompatibleTextGenerationModel.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,14 +163,14 @@ public function exposeParseResponseChoiceToCandidate(array $choiceData, int $ind
163163
return $this->parseResponseChoiceToCandidate($choiceData, $index);
164164
}
165165

166-
public function exposeParseResponseChoiceMessage(array $messageData): Message
166+
public function exposeParseResponseChoiceMessage(array $messageData, int $index = 0): Message
167167
{
168-
return $this->parseResponseChoiceMessage($messageData);
168+
return $this->parseResponseChoiceMessage($messageData, $index);
169169
}
170170

171-
public function exposeParseResponseChoiceMessageParts(array $messageData): array
171+
public function exposeParseResponseChoiceMessageParts(array $messageData, int $index = 0): array
172172
{
173-
return $this->parseResponseChoiceMessageParts($messageData);
173+
return $this->parseResponseChoiceMessageParts($messageData, $index);
174174
}
175175

176176
public function exposeParseResponseChoiceMessageToolCallPart(array $toolCallData): ?MessagePart

0 commit comments

Comments
 (0)