Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions src/Contracts/TransporterContract.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use OpenAI\Exceptions\ErrorException;
use OpenAI\Exceptions\TransporterException;
use OpenAI\Exceptions\UnserializableResponse;
use OpenAI\ValueObjects\Transporter\AdaptableResponse;
use OpenAI\ValueObjects\Transporter\Payload;
use OpenAI\ValueObjects\Transporter\Response;
use Psr\Http\Message\ResponseInterface;
Expand All @@ -17,16 +18,25 @@
interface TransporterContract
{
/**
* Sends a request to a server.
* Sends a request to a server expecting an object back.
*
* @return Response<array<array-key, mixed>|string>
* @return Response<array<array-key, mixed>>
*
* @throws ErrorException|UnserializableResponse|TransporterException
*/
public function requestObject(Payload $payload): Response;

/**
* Sends a content request to a server.
* Sends a request to a server expecting an adaptable response (object/string) back.
*
* @return AdaptableResponse<array<array-key, mixed>|string>
*
* @throws ErrorException|UnserializableResponse|TransporterException
*/
public function requestStringOrObject(Payload $payload): AdaptableResponse;

/**
* Sends a content request to a server expecting a string back.
*
* @throws ErrorException|TransporterException
*/
Expand Down
6 changes: 2 additions & 4 deletions src/Exceptions/UnserializableResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@

use Exception;
use JsonException;
use Psr\Http\Message\ResponseInterface;

final class UnserializableResponse extends Exception
{
/**
* Creates a new Exception instance.
*/
public function __construct(JsonException $exception)
public function __construct(JsonException $exception, public ResponseInterface $response)
{
parent::__construct($exception->getMessage(), 0, $exception);
}
Expand Down
4 changes: 2 additions & 2 deletions src/Resources/Audio.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public function transcribe(array $parameters): TranscriptionResponse
$payload = Payload::upload('audio/transcriptions', $parameters);

/** @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 */
$response = $this->transporter->requestObject($payload);
$response = $this->transporter->requestStringOrObject($payload);

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

/** @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 */
$response = $this->transporter->requestObject($payload);
$response = $this->transporter->requestStringOrObject($payload);

return TranslationResponse::from($response->data(), $response->meta());
}
Expand Down
5 changes: 3 additions & 2 deletions src/Responses/Audio/SpeechStreamResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ public function getIterator(): Generator

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

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

if ($meta instanceof \OpenAI\Responses\Meta\MetaInformation) {
foreach ($meta->toArray() as $key => $value) {
$response = $response->withHeader($key, (string) $value);
if (is_scalar($value)) {
$response = $response->withHeader($key, (string) $value);
}
}
}

Expand Down
39 changes: 35 additions & 4 deletions src/Responses/Meta/MetaInformation.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
use OpenAI\Responses\Concerns\ArrayAccessible;

/**
* @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}>
* @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>}>
*/
final class MetaInformation implements MetaInformationContract
{
/**
* @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}>
* @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>}>
*/
use ArrayAccessible;

Expand All @@ -20,20 +20,37 @@ private function __construct(
public readonly MetaInformationOpenAI $openai,
public readonly ?MetaInformationRateLimit $requestLimit,
public readonly ?MetaInformationRateLimit $tokenLimit,
public readonly MetaInformationCustom $custom,
) {}

/**
* @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
* @param array<string, array<int, string>> $headers
*/
public static function from(array $headers): self
{
$knownHeaders = [
'x-request-id',
'openai-model',
'openai-organization',
'openai-project',
'openai-version',
'openai-processing-ms',
'x-ratelimit-limit-requests',
'x-ratelimit-remaining-requests',
'x-ratelimit-reset-requests',
'x-ratelimit-limit-tokens',
'x-ratelimit-remaining-tokens',
'x-ratelimit-reset-tokens',
];

$headers = array_change_key_case($headers, CASE_LOWER);

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

$openai = MetaInformationOpenAI::from([
'model' => $headers['openai-model'][0] ?? null,
'organization' => $headers['openai-organization'][0] ?? null,
'project' => $headers['openai-project'][0] ?? null,
'version' => $headers['openai-version'][0] ?? null,
'processingMs' => isset($headers['openai-processing-ms'][0]) ? (int) $headers['openai-processing-ms'][0] : null,
]);
Expand All @@ -58,11 +75,23 @@ public static function from(array $headers): self
$tokenLimit = null;
}

$customHeaders = [];
foreach ($headers as $name => $values) {
if (in_array($name, $knownHeaders, true)) {
continue;
}

$customHeaders[$name] = $values[0] ?? null;
}

$custom = MetaInformationCustom::from($customHeaders);

return new self(
$requestId,
$openai,
$requestLimit,
$tokenLimit,
$custom,
);
}

Expand All @@ -74,6 +103,7 @@ public function toArray(): array
return array_filter([
'openai-model' => $this->openai->model,
'openai-organization' => $this->openai->organization,
'openai-project' => $this->openai->project,
'openai-processing-ms' => $this->openai->processingMs,
'openai-version' => $this->openai->version,
'x-ratelimit-limit-requests' => $this->requestLimit->limit ?? null,
Expand All @@ -83,6 +113,7 @@ public function toArray(): array
'x-ratelimit-reset-requests' => $this->requestLimit->reset ?? null,
'x-ratelimit-reset-tokens' => $this->tokenLimit->reset ?? null,
'x-request-id' => $this->requestId,
], fn (string|int|null $value): bool => ! is_null($value));
'custom' => ! $this->custom->isEmpty() ? $this->custom->toArray() : null,
], fn (array|string|int|null $value): bool => ! is_null($value));
}
}
34 changes: 34 additions & 0 deletions src/Responses/Meta/MetaInformationCustom.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace OpenAI\Responses\Meta;

final readonly class MetaInformationCustom
{
/**
* @param array<string, string> $headers
*/
private function __construct(
public array $headers
) {}

/**
* @param array<string, string|null> $headers
*/
public static function from(array $headers): self
{
return new self(array_filter($headers));
}

/**
* @return array<string, string>
*/
public function toArray(): array
{
return $this->headers;
}

public function isEmpty(): bool
{
return $this->headers === [];
}
}
4 changes: 3 additions & 1 deletion src/Responses/Meta/MetaInformationOpenAI.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,20 @@ final class MetaInformationOpenAI
private function __construct(
public readonly ?string $model,
public readonly ?string $organization,
public readonly ?string $project,
public readonly ?string $version,
public readonly ?int $processingMs,
) {}

/**
* @param array{model: ?string, organization: ?string, version: ?string, processingMs: ?int} $attributes
* @param array{model: ?string, organization: ?string, project: ?string, version: ?string, processingMs: ?int} $attributes
*/
public static function from(array $attributes): self
{
return new self(
$attributes['model'],
$attributes['organization'],
$attributes['project'],
$attributes['version'],
$attributes['processingMs'],
);
Expand Down
1 change: 0 additions & 1 deletion src/Responses/StreamResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ private function readLine(StreamInterface $stream): string

public function meta(): MetaInformation
{
// @phpstan-ignore-next-line
return MetaInformation::from($this->response->getHeaders());
}
}
32 changes: 28 additions & 4 deletions src/Transporters/HttpTransporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use OpenAI\Exceptions\ErrorException;
use OpenAI\Exceptions\TransporterException;
use OpenAI\Exceptions\UnserializableResponse;
use OpenAI\ValueObjects\Transporter\AdaptableResponse;
use OpenAI\ValueObjects\Transporter\BaseUri;
use OpenAI\ValueObjects\Transporter\Headers;
use OpenAI\ValueObjects\Transporter\Payload;
Expand Down Expand Up @@ -50,8 +51,31 @@ public function requestObject(Payload $payload): Response

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

$this->throwIfJsonError($response, $contents);

try {
/** @var array{error?: array{message: string, type: string, code: string}} $data */
$data = json_decode($contents, true, flags: JSON_THROW_ON_ERROR);
} catch (JsonException $jsonException) {
throw new UnserializableResponse($jsonException, $response);
}

return Response::from($data, $response->getHeaders());
}

/**
* {@inheritDoc}
*/
public function requestStringOrObject(Payload $payload): AdaptableResponse
{
$request = $payload->toRequest($this->baseUri, $this->headers, $this->queryParams);

$response = $this->sendRequest(fn (): \Psr\Http\Message\ResponseInterface => $this->client->sendRequest($request));

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

if (str_contains($response->getHeaderLine('Content-Type'), ContentType::TEXT_PLAIN->value)) {
return Response::from($contents, $response->getHeaders());
return AdaptableResponse::from($contents, $response->getHeaders());
}

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

return Response::from($data, $response->getHeaders());
return AdaptableResponse::from($data, $response->getHeaders());
}

/**
Expand Down Expand Up @@ -133,7 +157,7 @@ private function throwIfJsonError(ResponseInterface $response, string|ResponseIn
throw new ErrorException($response['error'], $statusCode);
}
} catch (JsonException $jsonException) {
throw new UnserializableResponse($jsonException);
throw new UnserializableResponse($jsonException, $response);
}
}
}
59 changes: 59 additions & 0 deletions src/ValueObjects/Transporter/AdaptableResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace OpenAI\ValueObjects\Transporter;

use OpenAI\Responses\Meta\MetaInformation;

/**
* @template-covariant TData of array|string
*
* @internal
*/
final readonly class AdaptableResponse
{
/**
* Creates a new AdaptableResponse value object.
*
* @param TData $data
*/
private function __construct(
private array|string $data,
private MetaInformation $meta
) {
// ..
}

/**
* Creates a new AdaptableResponse value object from the given data and meta information.
*
* @param TData $data
* @param array<string, array<int, string>> $headers
* @return AdaptableResponse<TData>
*/
public static function from(array|string $data, array $headers): self
{
$meta = MetaInformation::from($headers);

return new self($data, $meta);
}

/**
* Returns the response data.
*
* @return TData
*/
public function data(): array|string
{
return $this->data;
}

/**
* Returns the meta information.
*/
public function meta(): MetaInformation
{
return $this->meta;
}
}
Loading