diff --git a/src/Responses/Chat/CreateResponse.php b/src/Responses/Chat/CreateResponse.php index 57c1b654..88cb5c4a 100644 --- a/src/Responses/Chat/CreateResponse.php +++ b/src/Responses/Chat/CreateResponse.php @@ -12,7 +12,7 @@ use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @implements ResponseContract}, logprobs: ?array{content: ?array}>}, finish_reason: string|null}>, usage: array{prompt_tokens: int, completion_tokens: int|null, total_tokens: int}}> + * @implements ResponseContract}, logprobs: ?array{content: ?array}>}, finish_reason: string|null}>, usage?: array{prompt_tokens: int, completion_tokens: int|null, total_tokens: int}}> */ final class CreateResponse implements ResponseContract, ResponseHasMetaInformationContract { @@ -28,20 +28,20 @@ final class CreateResponse implements ResponseContract, ResponseHasMetaInformati * @param array $choices */ private function __construct( - public readonly string $id, + public readonly ?string $id, public readonly string $object, public readonly int $created, public readonly string $model, public readonly ?string $systemFingerprint, public readonly array $choices, - public readonly CreateResponseUsage $usage, + public readonly ?CreateResponseUsage $usage, private readonly MetaInformation $meta, ) {} /** * Acts as static factory, and returns a new Response instance. * - * @param array{id: string, object: string, created: int, model: string, system_fingerprint?: string, choices: array}, logprobs: ?array{content: ?array}>}, finish_reason: string|null}>, usage: array{prompt_tokens: int, completion_tokens: int|null, total_tokens: int, prompt_tokens_details?:array{cached_tokens:int}, completion_tokens_details?:array{audio_tokens?:int, reasoning_tokens:int, accepted_prediction_tokens:int, rejected_prediction_tokens:int}}} $attributes + * @param array{id?: string, object: string, created: int, model: string, system_fingerprint?: string, choices: array}, logprobs: ?array{content: ?array}>}, finish_reason: string|null}>, usage?: array{prompt_tokens: int, completion_tokens: int|null, total_tokens: int, prompt_tokens_details?:array{cached_tokens:int}, completion_tokens_details?:array{audio_tokens?:int, reasoning_tokens:int, accepted_prediction_tokens:int, rejected_prediction_tokens:int}}} $attributes */ public static function from(array $attributes, MetaInformation $meta): self { @@ -50,13 +50,13 @@ public static function from(array $attributes, MetaInformation $meta): self ), $attributes['choices']); return new self( - $attributes['id'], + $attributes['id'] ?? null, $attributes['object'], $attributes['created'], $attributes['model'], $attributes['system_fingerprint'] ?? null, $choices, - CreateResponseUsage::from($attributes['usage']), + isset($attributes['usage']) ? CreateResponseUsage::from($attributes['usage']) : null, $meta, ); } @@ -76,7 +76,7 @@ public function toArray(): array static fn (CreateResponseChoice $result): array => $result->toArray(), $this->choices, ), - 'usage' => $this->usage->toArray(), + 'usage' => $this->usage?->toArray(), ], fn (mixed $value): bool => ! is_null($value)); } } diff --git a/src/Responses/Chat/CreateStreamedResponse.php b/src/Responses/Chat/CreateStreamedResponse.php index adce31d7..6eb06ec6 100644 --- a/src/Responses/Chat/CreateStreamedResponse.php +++ b/src/Responses/Chat/CreateStreamedResponse.php @@ -9,7 +9,7 @@ use OpenAI\Testing\Responses\Concerns\FakeableForStreamedResponse; /** - * @implements ResponseContract, usage?: array{prompt_tokens: int, completion_tokens: int|null, total_tokens: int}}> + * @implements ResponseContract, usage?: array{prompt_tokens: int, completion_tokens: int|null, total_tokens: int}}> */ final class CreateStreamedResponse implements ResponseContract { @@ -24,7 +24,7 @@ final class CreateStreamedResponse implements ResponseContract * @param array $choices */ private function __construct( - public readonly string $id, + public readonly ?string $id, public readonly string $object, public readonly int $created, public readonly string $model, @@ -35,7 +35,7 @@ private function __construct( /** * Acts as static factory, and returns a new Response instance. * - * @param array{id: string, object: string, created: int, model: string, choices: array, usage?: array{prompt_tokens: int, completion_tokens: int|null, total_tokens: int}} $attributes + * @param array{id?: string, object: string, created: int, model: string, choices: array, usage?: array{prompt_tokens: int, completion_tokens: int|null, total_tokens: int}} $attributes */ public static function from(array $attributes): self { @@ -44,7 +44,7 @@ public static function from(array $attributes): self ), $attributes['choices']); return new self( - $attributes['id'], + $attributes['id'] ?? null, $attributes['object'], $attributes['created'], $attributes['model'], @@ -58,7 +58,7 @@ public static function from(array $attributes): self */ public function toArray(): array { - $data = [ + return array_filter([ 'id' => $this->id, 'object' => $this->object, 'created' => $this->created, @@ -67,12 +67,7 @@ public function toArray(): array static fn (CreateStreamedResponseChoice $result): array => $result->toArray(), $this->choices, ), - ]; - - if ($this->usage instanceof \OpenAI\Responses\Chat\CreateResponseUsage) { - $data['usage'] = $this->usage->toArray(); - } - - return $data; + 'usage' => $this->usage?->toArray(), + ], fn (mixed $value): bool => ! is_null($value)); } } diff --git a/src/Responses/Embeddings/CreateResponse.php b/src/Responses/Embeddings/CreateResponse.php index 7c734f12..0bd29104 100644 --- a/src/Responses/Embeddings/CreateResponse.php +++ b/src/Responses/Embeddings/CreateResponse.php @@ -12,12 +12,12 @@ use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @implements ResponseContract, index: int}>, usage: array{prompt_tokens: int, total_tokens: int}}> + * @implements ResponseContract, index?: int}>, usage?: array{prompt_tokens: int, total_tokens: int}}> */ final class CreateResponse implements ResponseContract, ResponseHasMetaInformationContract { /** - * @use ArrayAccessible, index: int}>, usage: array{prompt_tokens: int, total_tokens: int}}> + * @use ArrayAccessible, index?: int}>, usage?: array{prompt_tokens: int, total_tokens: int}}> */ use ArrayAccessible; @@ -30,14 +30,14 @@ final class CreateResponse implements ResponseContract, ResponseHasMetaInformati private function __construct( public readonly string $object, public readonly array $embeddings, - public readonly CreateResponseUsage $usage, + public readonly ?CreateResponseUsage $usage, private readonly MetaInformation $meta, ) {} /** * Acts as static factory, and returns a new Response instance. * - * @param array{object: string, data: array, index: int}>, usage: array{prompt_tokens: int, total_tokens: int}} $attributes + * @param array{object: string, data: array, index?: int}>, usage?: array{prompt_tokens: int, total_tokens: int}} $attributes */ public static function from(array $attributes, MetaInformation $meta): self { @@ -48,7 +48,7 @@ public static function from(array $attributes, MetaInformation $meta): self return new self( $attributes['object'], $embeddings, - CreateResponseUsage::from($attributes['usage']), + isset($attributes['usage']) ? CreateResponseUsage::from($attributes['usage']) : null, $meta, ); } @@ -58,13 +58,13 @@ public static function from(array $attributes, MetaInformation $meta): self */ public function toArray(): array { - return [ + return array_filter([ 'object' => $this->object, 'data' => array_map( static fn (CreateResponseEmbedding $result): array => $result->toArray(), $this->embeddings, ), - 'usage' => $this->usage->toArray(), - ]; + 'usage' => $this->usage?->toArray(), + ], fn (mixed $value): bool => ! is_null($value)); } } diff --git a/src/Responses/Embeddings/CreateResponseEmbedding.php b/src/Responses/Embeddings/CreateResponseEmbedding.php index 2c6c4635..c62b1c65 100644 --- a/src/Responses/Embeddings/CreateResponseEmbedding.php +++ b/src/Responses/Embeddings/CreateResponseEmbedding.php @@ -11,31 +11,31 @@ final class CreateResponseEmbedding */ private function __construct( public readonly string $object, - public readonly int $index, + public readonly ?int $index, public readonly array $embedding, ) {} /** - * @param array{object: string, index: int, embedding: array} $attributes + * @param array{object: string, index?: int, embedding: array} $attributes */ public static function from(array $attributes): self { return new self( $attributes['object'], - $attributes['index'], + $attributes['index'] ?? null, $attributes['embedding'], ); } /** - * @return array{object: string, index: int, embedding: array} + * @return array{object: string, index?: int, embedding: array} */ public function toArray(): array { - return [ + return array_filter([ 'object' => $this->object, 'index' => $this->index, 'embedding' => $this->embedding, - ]; + ], fn (mixed $value): bool => ! is_null($value)); } } diff --git a/tests/Fixtures/Chat.php b/tests/Fixtures/Chat.php index a15795df..f19b14e6 100644 --- a/tests/Fixtures/Chat.php +++ b/tests/Fixtures/Chat.php @@ -37,6 +37,64 @@ function chatCompletion(): array ]; } +/** + * @return array + */ +function chatCompletionWithoutId(): array +{ + return [ + 'object' => 'chat.completion', + 'created' => 1677652288, + 'model' => 'gpt-3.5-turbo', + 'choices' => [ + [ + 'index' => 0, + 'message' => [ + 'role' => 'assistant', + 'content' => "\n\nHello there, how may I assist you today?", + ], + 'finish_reason' => 'stop', + ], + ], + 'usage' => [ + 'prompt_tokens' => 9, + 'completion_tokens' => 12, + 'total_tokens' => 21, + 'prompt_tokens_details' => [ + 'cached_tokens' => 5, + ], + 'completion_tokens_details' => [ + 'reasoning_tokens' => 0, + 'accepted_prediction_tokens' => 0, + 'rejected_prediction_tokens' => 0, + ], + ], + ]; +} + +/** + * @return array + */ +function chatCompletionWithoutUsage(): array +{ + return [ + 'id' => 'chatcmpl-123', + 'object' => 'chat.completion', + 'created' => 1677652288, + 'model' => 'gpt-3.5-turbo', + 'choices' => [ + [ + 'index' => 0, + 'message' => [ + 'role' => 'assistant', + 'content' => "\n\nHello there, how may I assist you today?", + ], + 'finish_reason' => 'stop', + ], + ], + ]; +} + /** * @return array */ @@ -242,6 +300,24 @@ function chatCompletionStreamFirstChunk(): array ]; } +function chatCompletionStreamFirstChunkWithoutId(): array +{ + return [ + 'object' => 'chat.completion.chunk', + 'created' => 1679432086, + 'model' => 'gpt-4-0314', + 'choices' => [ + [ + 'index' => 0, + 'delta' => [ + 'role' => 'assistant', + ], + 'finish_reason' => null, + ], + ], + ]; +} + function chatCompletionStreamContentChunk(): array { return [ diff --git a/tests/Fixtures/Embedding.php b/tests/Fixtures/Embedding.php index 659f6a69..dbddee7e 100644 --- a/tests/Fixtures/Embedding.php +++ b/tests/Fixtures/Embedding.php @@ -16,6 +16,21 @@ function embedding(): array ]; } +/** + * @return array + */ +function embeddingWithoutIndex(): array +{ + return [ + 'object' => 'embedding', + 'embedding' => [ + -0.008906792, + -0.013743395, + 0.009874112, + ], + ]; +} + /** * @return array */ @@ -33,3 +48,17 @@ function embeddingList(): array ], ]; } + +/** + * @return array + */ +function embeddingListWithoutUsage(): array +{ + return [ + 'object' => 'list', + 'data' => [ + embedding(), + embedding(), + ], + ]; +} diff --git a/tests/Responses/Chat/CreateResponse.php b/tests/Responses/Chat/CreateResponse.php index 565d72c0..caafedf5 100644 --- a/tests/Responses/Chat/CreateResponse.php +++ b/tests/Responses/Chat/CreateResponse.php @@ -21,6 +21,38 @@ ->meta()->toBeInstanceOf(MetaInformation::class); }); +test('from without id', function () { + $completion = CreateResponse::from(chatCompletionWithoutId(), meta()); + + expect($completion) + ->toBeInstanceOf(CreateResponse::class) + ->id->toBeNull() + ->object->toBe('chat.completion') + ->created->toBe(1677652288) + ->model->toBe('gpt-3.5-turbo') + ->systemFingerprint->toBeNull() + ->choices->toBeArray()->toHaveCount(1) + ->choices->each->toBeInstanceOf(CreateResponseChoice::class) + ->usage->toBeInstanceOf(CreateResponseUsage::class) + ->meta()->toBeInstanceOf(MetaInformation::class); +}); + +test('from without usage', function () { + $completion = CreateResponse::from(chatCompletionWithoutUsage(), meta()); + + expect($completion) + ->toBeInstanceOf(CreateResponse::class) + ->id->toBe('chatcmpl-123') + ->object->toBe('chat.completion') + ->created->toBe(1677652288) + ->model->toBe('gpt-3.5-turbo') + ->systemFingerprint->toBeNull() + ->choices->toBeArray()->toHaveCount(1) + ->choices->each->toBeInstanceOf(CreateResponseChoice::class) + ->usage->toBeNull() + ->meta()->toBeInstanceOf(MetaInformation::class); +}); + test('from with system fingerprint', function () { $completion = CreateResponse::from(chatCompletionWithSystemFingerprint(), meta()); diff --git a/tests/Responses/Chat/CreateStreamedResponse.php b/tests/Responses/Chat/CreateStreamedResponse.php index 4995400b..80a09e54 100644 --- a/tests/Responses/Chat/CreateStreamedResponse.php +++ b/tests/Responses/Chat/CreateStreamedResponse.php @@ -17,6 +17,19 @@ ->choices->each->toBeInstanceOf(CreateStreamedResponseChoice::class); }); +test('from without id', function () { + $completion = CreateStreamedResponse::from(chatCompletionStreamFirstChunkWithoutId()); + + expect($completion) + ->toBeInstanceOf(CreateStreamedResponse::class) + ->id->toBeNull() + ->object->toBe('chat.completion.chunk') + ->created->toBe(1679432086) + ->model->toBe('gpt-4-0314') + ->choices->toBeArray()->toHaveCount(1) + ->choices->each->toBeInstanceOf(CreateStreamedResponseChoice::class); +}); + test('from usage chunk', function () { $completion = CreateStreamedResponse::from(chatCompletionStreamUsageChunk()); diff --git a/tests/Responses/Embeddings/CreateResponse.php b/tests/Responses/Embeddings/CreateResponse.php index 8c6b120a..0266bd1b 100644 --- a/tests/Responses/Embeddings/CreateResponse.php +++ b/tests/Responses/Embeddings/CreateResponse.php @@ -2,6 +2,7 @@ use OpenAI\Responses\Embeddings\CreateResponse; use OpenAI\Responses\Embeddings\CreateResponseEmbedding; +use OpenAI\Responses\Embeddings\CreateResponseUsage; use OpenAI\Responses\Meta\MetaInformation; test('from', function () { @@ -12,6 +13,19 @@ ->object->toBe('list') ->embeddings->toBeArray()->toHaveCount(2) ->embeddings->each->toBeInstanceOf(CreateResponseEmbedding::class) + ->usage->toBeInstanceOf(CreateResponseUsage::class) + ->meta()->toBeInstanceOf(MetaInformation::class); +}); + +test('from without usage', function () { + $response = CreateResponse::from(embeddingListWithoutUsage(), meta()); + + expect($response) + ->toBeInstanceOf(CreateResponse::class) + ->object->toBe('list') + ->embeddings->toBeArray()->toHaveCount(2) + ->embeddings->each->toBeInstanceOf(CreateResponseEmbedding::class) + ->usage->toBeNull() ->meta()->toBeInstanceOf(MetaInformation::class); }); diff --git a/tests/Responses/Embeddings/CreateResponseEmbedding.php b/tests/Responses/Embeddings/CreateResponseEmbedding.php index 220658aa..a5fed133 100644 --- a/tests/Responses/Embeddings/CreateResponseEmbedding.php +++ b/tests/Responses/Embeddings/CreateResponseEmbedding.php @@ -15,6 +15,19 @@ ]); }); +test('from without index', function () { + $result = CreateResponseEmbedding::from(embeddingWithoutIndex()); + + expect($result) + ->object->toBe('embedding') + ->index->toBeNull() + ->embedding->toBeArray()->toBe([ + -0.008906792, + -0.013743395, + 0.009874112, + ]); +}); + test('to array', function () { $result = CreateResponseEmbedding::from(embedding());