Skip to content

Commit 0e1ec5a

Browse files
authored
chore(meta): rework exception handling (#643)
* chore(meta): rework exception handling * chore: introduce AdaptableResponse (string or object) * chore: test object | string/object the same * feat: add support for custom headers + openai-project * chore: handle addition of 'custom' array to meta headers * chore: support malformed custom headers * chore: prefer strict type over mixed
1 parent 32e8a0b commit 0e1ec5a

File tree

18 files changed

+301
-75
lines changed

18 files changed

+301
-75
lines changed

src/Contracts/TransporterContract.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use OpenAI\Exceptions\ErrorException;
88
use OpenAI\Exceptions\TransporterException;
99
use OpenAI\Exceptions\UnserializableResponse;
10+
use OpenAI\ValueObjects\Transporter\AdaptableResponse;
1011
use OpenAI\ValueObjects\Transporter\Payload;
1112
use OpenAI\ValueObjects\Transporter\Response;
1213
use Psr\Http\Message\ResponseInterface;
@@ -17,16 +18,25 @@
1718
interface TransporterContract
1819
{
1920
/**
20-
* Sends a request to a server.
21+
* Sends a request to a server expecting an object back.
2122
*
22-
* @return Response<array<array-key, mixed>|string>
23+
* @return Response<array<array-key, mixed>>
2324
*
2425
* @throws ErrorException|UnserializableResponse|TransporterException
2526
*/
2627
public function requestObject(Payload $payload): Response;
2728

2829
/**
29-
* Sends a content request to a server.
30+
* Sends a request to a server expecting an adaptable response (object/string) back.
31+
*
32+
* @return AdaptableResponse<array<array-key, mixed>|string>
33+
*
34+
* @throws ErrorException|UnserializableResponse|TransporterException
35+
*/
36+
public function requestStringOrObject(Payload $payload): AdaptableResponse;
37+
38+
/**
39+
* Sends a content request to a server expecting a string back.
3040
*
3141
* @throws ErrorException|TransporterException
3242
*/

src/Exceptions/UnserializableResponse.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,11 @@
66

77
use Exception;
88
use JsonException;
9+
use Psr\Http\Message\ResponseInterface;
910

1011
final class UnserializableResponse extends Exception
1112
{
12-
/**
13-
* Creates a new Exception instance.
14-
*/
15-
public function __construct(JsonException $exception)
13+
public function __construct(JsonException $exception, public ResponseInterface $response)
1614
{
1715
parent::__construct($exception->getMessage(), 0, $exception);
1816
}

src/Resources/Audio.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public function transcribe(array $parameters): TranscriptionResponse
6464
$payload = Payload::upload('audio/transcriptions', $parameters);
6565

6666
/** @var Response<array{task: ?string, language: ?string, duration: ?float, segments: array<int, array{id: int, seek: int, start: float, end: float, text: string, tokens: array<int, int>, temperature: float, avg_logprob: float, compression_ratio: float, no_speech_prob: float, transient?: bool}>, words: array<int, array{word: string, start: float, end: float}>, text: string}> $response */
67-
$response = $this->transporter->requestObject($payload);
67+
$response = $this->transporter->requestStringOrObject($payload);
6868

6969
return TranscriptionResponse::from($response->data(), $response->meta());
7070
}
@@ -100,7 +100,7 @@ public function translate(array $parameters): TranslationResponse
100100
$payload = Payload::upload('audio/translations', $parameters);
101101

102102
/** @var Response<array{task: ?string, language: ?string, duration: ?float, segments: array<int, array{id: int, seek: int, start: float, end: float, text: string, tokens: array<int, int>, temperature: float, avg_logprob: float, compression_ratio: float, no_speech_prob: float, transient?: bool}>, text: string}> $response */
103-
$response = $this->transporter->requestObject($payload);
103+
$response = $this->transporter->requestStringOrObject($payload);
104104

105105
return TranslationResponse::from($response->data(), $response->meta());
106106
}

src/Responses/Audio/SpeechStreamResponse.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ public function getIterator(): Generator
3232

3333
public function meta(): MetaInformation
3434
{
35-
// @phpstan-ignore-next-line
3635
return MetaInformation::from($this->response->getHeaders());
3736
}
3837

@@ -44,7 +43,9 @@ public static function fake(?string $content = null, ?MetaInformation $meta = nu
4443

4544
if ($meta instanceof \OpenAI\Responses\Meta\MetaInformation) {
4645
foreach ($meta->toArray() as $key => $value) {
47-
$response = $response->withHeader($key, (string) $value);
46+
if (is_scalar($value)) {
47+
$response = $response->withHeader($key, (string) $value);
48+
}
4849
}
4950
}
5051

src/Responses/Meta/MetaInformation.php

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
use OpenAI\Responses\Concerns\ArrayAccessible;
77

88
/**
9-
* @implements MetaInformationContract<array{x-request-id?: string, openai-model?: string, openai-organization?: string, openai-processing-ms?: int, openai-version?: string, x-ratelimit-limit-requests?: int, x-ratelimit-limit-tokens?: int, x-ratelimit-remaining-requests?: int, x-ratelimit-remaining-tokens?: int, x-ratelimit-reset-requests?: string, x-ratelimit-reset-tokens?: string}>
9+
* @implements MetaInformationContract<array{x-request-id?: string, openai-model?: string, openai-organization?: string, openai-project?: string, openai-processing-ms?: int, openai-version?: string, x-ratelimit-limit-requests?: int, x-ratelimit-limit-tokens?: int, x-ratelimit-remaining-requests?: int, x-ratelimit-remaining-tokens?: int, x-ratelimit-reset-requests?: string, x-ratelimit-reset-tokens?: string, custom?: array<string, string>}>
1010
*/
1111
final class MetaInformation implements MetaInformationContract
1212
{
1313
/**
14-
* @use ArrayAccessible<array{x-request-id?: string, openai-model?: string, openai-organization?: string, openai-processing-ms?: int, openai-version?: string, x-ratelimit-limit-requests?: int, x-ratelimit-limit-tokens?: int, x-ratelimit-remaining-requests?: int, x-ratelimit-remaining-tokens?: int, x-ratelimit-reset-requests?: string, x-ratelimit-reset-tokens?: string}>
14+
* @use ArrayAccessible<array{x-request-id?: string, openai-model?: string, openai-organization?: string, openai-project?: string, openai-processing-ms?: int, openai-version?: string, x-ratelimit-limit-requests?: int, x-ratelimit-limit-tokens?: int, x-ratelimit-remaining-requests?: int, x-ratelimit-remaining-tokens?: int, x-ratelimit-reset-requests?: string, x-ratelimit-reset-tokens?: string, custom?: array<string, string>}>
1515
*/
1616
use ArrayAccessible;
1717

@@ -20,20 +20,37 @@ private function __construct(
2020
public readonly MetaInformationOpenAI $openai,
2121
public readonly ?MetaInformationRateLimit $requestLimit,
2222
public readonly ?MetaInformationRateLimit $tokenLimit,
23+
public readonly MetaInformationCustom $custom,
2324
) {}
2425

2526
/**
26-
* @param array{x-request-id: string[], openai-model: string[], openai-organization: string[], openai-version: string[], openai-processing-ms: string[], x-ratelimit-limit-requests: string[], x-ratelimit-remaining-requests: string[], x-ratelimit-reset-requests: string[], x-ratelimit-limit-tokens: string[], x-ratelimit-remaining-tokens: string[], x-ratelimit-reset-tokens: string[]} $headers
27+
* @param array<string, array<int, string>> $headers
2728
*/
2829
public static function from(array $headers): self
2930
{
31+
$knownHeaders = [
32+
'x-request-id',
33+
'openai-model',
34+
'openai-organization',
35+
'openai-project',
36+
'openai-version',
37+
'openai-processing-ms',
38+
'x-ratelimit-limit-requests',
39+
'x-ratelimit-remaining-requests',
40+
'x-ratelimit-reset-requests',
41+
'x-ratelimit-limit-tokens',
42+
'x-ratelimit-remaining-tokens',
43+
'x-ratelimit-reset-tokens',
44+
];
45+
3046
$headers = array_change_key_case($headers, CASE_LOWER);
3147

3248
$requestId = $headers['x-request-id'][0] ?? null;
3349

3450
$openai = MetaInformationOpenAI::from([
3551
'model' => $headers['openai-model'][0] ?? null,
3652
'organization' => $headers['openai-organization'][0] ?? null,
53+
'project' => $headers['openai-project'][0] ?? null,
3754
'version' => $headers['openai-version'][0] ?? null,
3855
'processingMs' => isset($headers['openai-processing-ms'][0]) ? (int) $headers['openai-processing-ms'][0] : null,
3956
]);
@@ -58,11 +75,23 @@ public static function from(array $headers): self
5875
$tokenLimit = null;
5976
}
6077

78+
$customHeaders = [];
79+
foreach ($headers as $name => $values) {
80+
if (in_array($name, $knownHeaders, true)) {
81+
continue;
82+
}
83+
84+
$customHeaders[$name] = $values[0] ?? null;
85+
}
86+
87+
$custom = MetaInformationCustom::from($customHeaders);
88+
6189
return new self(
6290
$requestId,
6391
$openai,
6492
$requestLimit,
6593
$tokenLimit,
94+
$custom,
6695
);
6796
}
6897

@@ -74,6 +103,7 @@ public function toArray(): array
74103
return array_filter([
75104
'openai-model' => $this->openai->model,
76105
'openai-organization' => $this->openai->organization,
106+
'openai-project' => $this->openai->project,
77107
'openai-processing-ms' => $this->openai->processingMs,
78108
'openai-version' => $this->openai->version,
79109
'x-ratelimit-limit-requests' => $this->requestLimit->limit ?? null,
@@ -83,6 +113,7 @@ public function toArray(): array
83113
'x-ratelimit-reset-requests' => $this->requestLimit->reset ?? null,
84114
'x-ratelimit-reset-tokens' => $this->tokenLimit->reset ?? null,
85115
'x-request-id' => $this->requestId,
86-
], fn (string|int|null $value): bool => ! is_null($value));
116+
'custom' => ! $this->custom->isEmpty() ? $this->custom->toArray() : null,
117+
], fn (array|string|int|null $value): bool => ! is_null($value));
87118
}
88119
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace OpenAI\Responses\Meta;
4+
5+
final readonly class MetaInformationCustom
6+
{
7+
/**
8+
* @param array<string, string> $headers
9+
*/
10+
private function __construct(
11+
public array $headers
12+
) {}
13+
14+
/**
15+
* @param array<string, string|null> $headers
16+
*/
17+
public static function from(array $headers): self
18+
{
19+
return new self(array_filter($headers));
20+
}
21+
22+
/**
23+
* @return array<string, string>
24+
*/
25+
public function toArray(): array
26+
{
27+
return $this->headers;
28+
}
29+
30+
public function isEmpty(): bool
31+
{
32+
return $this->headers === [];
33+
}
34+
}

src/Responses/Meta/MetaInformationOpenAI.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,20 @@ final class MetaInformationOpenAI
77
private function __construct(
88
public readonly ?string $model,
99
public readonly ?string $organization,
10+
public readonly ?string $project,
1011
public readonly ?string $version,
1112
public readonly ?int $processingMs,
1213
) {}
1314

1415
/**
15-
* @param array{model: ?string, organization: ?string, version: ?string, processingMs: ?int} $attributes
16+
* @param array{model: ?string, organization: ?string, project: ?string, version: ?string, processingMs: ?int} $attributes
1617
*/
1718
public static function from(array $attributes): self
1819
{
1920
return new self(
2021
$attributes['model'],
2122
$attributes['organization'],
23+
$attributes['project'],
2224
$attributes['version'],
2325
$attributes['processingMs'],
2426
);

src/Responses/StreamResponse.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@ private function readLine(StreamInterface $stream): string
9595

9696
public function meta(): MetaInformation
9797
{
98-
// @phpstan-ignore-next-line
9998
return MetaInformation::from($this->response->getHeaders());
10099
}
101100
}

src/Transporters/HttpTransporter.php

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use OpenAI\Exceptions\ErrorException;
1313
use OpenAI\Exceptions\TransporterException;
1414
use OpenAI\Exceptions\UnserializableResponse;
15+
use OpenAI\ValueObjects\Transporter\AdaptableResponse;
1516
use OpenAI\ValueObjects\Transporter\BaseUri;
1617
use OpenAI\ValueObjects\Transporter\Headers;
1718
use OpenAI\ValueObjects\Transporter\Payload;
@@ -50,8 +51,31 @@ public function requestObject(Payload $payload): Response
5051

5152
$contents = (string) $response->getBody();
5253

54+
$this->throwIfJsonError($response, $contents);
55+
56+
try {
57+
/** @var array{error?: array{message: string, type: string, code: string}} $data */
58+
$data = json_decode($contents, true, flags: JSON_THROW_ON_ERROR);
59+
} catch (JsonException $jsonException) {
60+
throw new UnserializableResponse($jsonException, $response);
61+
}
62+
63+
return Response::from($data, $response->getHeaders());
64+
}
65+
66+
/**
67+
* {@inheritDoc}
68+
*/
69+
public function requestStringOrObject(Payload $payload): AdaptableResponse
70+
{
71+
$request = $payload->toRequest($this->baseUri, $this->headers, $this->queryParams);
72+
73+
$response = $this->sendRequest(fn (): \Psr\Http\Message\ResponseInterface => $this->client->sendRequest($request));
74+
75+
$contents = (string) $response->getBody();
76+
5377
if (str_contains($response->getHeaderLine('Content-Type'), ContentType::TEXT_PLAIN->value)) {
54-
return Response::from($contents, $response->getHeaders());
78+
return AdaptableResponse::from($contents, $response->getHeaders());
5579
}
5680

5781
$this->throwIfJsonError($response, $contents);
@@ -60,10 +84,10 @@ public function requestObject(Payload $payload): Response
6084
/** @var array{error?: array{message: string, type: string, code: string}} $data */
6185
$data = json_decode($contents, true, flags: JSON_THROW_ON_ERROR);
6286
} catch (JsonException $jsonException) {
63-
throw new UnserializableResponse($jsonException);
87+
throw new UnserializableResponse($jsonException, $response);
6488
}
6589

66-
return Response::from($data, $response->getHeaders());
90+
return AdaptableResponse::from($data, $response->getHeaders());
6791
}
6892

6993
/**
@@ -133,7 +157,7 @@ private function throwIfJsonError(ResponseInterface $response, string|ResponseIn
133157
throw new ErrorException($response['error'], $statusCode);
134158
}
135159
} catch (JsonException $jsonException) {
136-
throw new UnserializableResponse($jsonException);
160+
throw new UnserializableResponse($jsonException, $response);
137161
}
138162
}
139163
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenAI\ValueObjects\Transporter;
6+
7+
use OpenAI\Responses\Meta\MetaInformation;
8+
9+
/**
10+
* @template-covariant TData of array|string
11+
*
12+
* @internal
13+
*/
14+
final readonly class AdaptableResponse
15+
{
16+
/**
17+
* Creates a new AdaptableResponse value object.
18+
*
19+
* @param TData $data
20+
*/
21+
private function __construct(
22+
private array|string $data,
23+
private MetaInformation $meta
24+
) {
25+
// ..
26+
}
27+
28+
/**
29+
* Creates a new AdaptableResponse value object from the given data and meta information.
30+
*
31+
* @param TData $data
32+
* @param array<string, array<int, string>> $headers
33+
* @return AdaptableResponse<TData>
34+
*/
35+
public static function from(array|string $data, array $headers): self
36+
{
37+
$meta = MetaInformation::from($headers);
38+
39+
return new self($data, $meta);
40+
}
41+
42+
/**
43+
* Returns the response data.
44+
*
45+
* @return TData
46+
*/
47+
public function data(): array|string
48+
{
49+
return $this->data;
50+
}
51+
52+
/**
53+
* Returns the meta information.
54+
*/
55+
public function meta(): MetaInformation
56+
{
57+
return $this->meta;
58+
}
59+
}

0 commit comments

Comments
 (0)