From 72a83ad2ae1ba4bc6348887f303da4089ba1a1ce Mon Sep 17 00:00:00 2001 From: EJLin Date: Thu, 21 Nov 2024 16:38:55 +0800 Subject: [PATCH 1/3] feat: add openai compatibility support for google gemini --- src/Responses/Chat/CreateResponse.php | 8 +- src/Responses/Chat/CreateStreamedResponse.php | 4 +- src/Responses/Embeddings/CreateResponse.php | 4 +- .../Embeddings/CreateResponseEmbedding.php | 4 +- tests/Fixtures/Chat.php | 76 +++++++++++++++++++ tests/Fixtures/Embedding.php | 29 +++++++ tests/Responses/Chat/CreateResponse.php | 32 ++++++++ .../Responses/Chat/CreateStreamedResponse.php | 13 ++++ tests/Responses/Embeddings/CreateResponse.php | 14 ++++ .../Embeddings/CreateResponseEmbedding.php | 13 ++++ 10 files changed, 187 insertions(+), 10 deletions(-) diff --git a/src/Responses/Chat/CreateResponse.php b/src/Responses/Chat/CreateResponse.php index 3d424e8b..6d9c99dd 100644 --- a/src/Responses/Chat/CreateResponse.php +++ b/src/Responses/Chat/CreateResponse.php @@ -28,13 +28,13 @@ 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, ) {} @@ -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, ); } diff --git a/src/Responses/Chat/CreateStreamedResponse.php b/src/Responses/Chat/CreateStreamedResponse.php index adce31d7..afb5150a 100644 --- a/src/Responses/Chat/CreateStreamedResponse.php +++ b/src/Responses/Chat/CreateStreamedResponse.php @@ -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, @@ -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'], diff --git a/src/Responses/Embeddings/CreateResponse.php b/src/Responses/Embeddings/CreateResponse.php index 7c734f12..2762550c 100644 --- a/src/Responses/Embeddings/CreateResponse.php +++ b/src/Responses/Embeddings/CreateResponse.php @@ -30,7 +30,7 @@ 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, ) {} @@ -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, ); } diff --git a/src/Responses/Embeddings/CreateResponseEmbedding.php b/src/Responses/Embeddings/CreateResponseEmbedding.php index 2c6c4635..e7816c88 100644 --- a/src/Responses/Embeddings/CreateResponseEmbedding.php +++ b/src/Responses/Embeddings/CreateResponseEmbedding.php @@ -11,7 +11,7 @@ final class CreateResponseEmbedding */ private function __construct( public readonly string $object, - public readonly int $index, + public readonly ?int $index, public readonly array $embedding, ) {} @@ -22,7 +22,7 @@ public static function from(array $attributes): self { return new self( $attributes['object'], - $attributes['index'], + $attributes['index'] ?? null, $attributes['embedding'], ); } diff --git a/tests/Fixtures/Chat.php b/tests/Fixtures/Chat.php index 9d9c5ed5..aa0138c1 100644 --- a/tests/Fixtures/Chat.php +++ b/tests/Fixtures/Chat.php @@ -36,6 +36,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 */ @@ -195,6 +253,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..f495392e 100644 --- a/tests/Responses/Embeddings/CreateResponse.php +++ b/tests/Responses/Embeddings/CreateResponse.php @@ -1,6 +1,7 @@ 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()); From c7d0be940dc25856f51438bb295961366b514165 Mon Sep 17 00:00:00 2001 From: EJLin Date: Thu, 21 Nov 2024 16:53:22 +0800 Subject: [PATCH 2/3] fix: coding style checks --- src/Responses/Chat/CreateResponse.php | 2 +- tests/Responses/Embeddings/CreateResponse.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Responses/Chat/CreateResponse.php b/src/Responses/Chat/CreateResponse.php index 6d9c99dd..f8e4db4d 100644 --- a/src/Responses/Chat/CreateResponse.php +++ b/src/Responses/Chat/CreateResponse.php @@ -56,7 +56,7 @@ public static function from(array $attributes, MetaInformation $meta): self $attributes['model'], $attributes['system_fingerprint'] ?? null, $choices, - isset($attributes['usage']) ? CreateResponseUsage::from($attributes['usage']) : null, + isset($attributes['usage']) ? CreateResponseUsage::from($attributes['usage']) : null, $meta, ); } diff --git a/tests/Responses/Embeddings/CreateResponse.php b/tests/Responses/Embeddings/CreateResponse.php index f495392e..0266bd1b 100644 --- a/tests/Responses/Embeddings/CreateResponse.php +++ b/tests/Responses/Embeddings/CreateResponse.php @@ -1,8 +1,8 @@ Date: Thu, 21 Nov 2024 17:40:22 +0800 Subject: [PATCH 3/3] fix: phpstan analyse --- src/Responses/Chat/CreateResponse.php | 6 +++--- src/Responses/Chat/CreateStreamedResponse.php | 15 +++++---------- src/Responses/Embeddings/CreateResponse.php | 12 ++++++------ .../Embeddings/CreateResponseEmbedding.php | 8 ++++---- 4 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/Responses/Chat/CreateResponse.php b/src/Responses/Chat/CreateResponse.php index f8e4db4d..7fbcec4e 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}, finish_reason: string|null}>, usage: array{prompt_tokens: int, completion_tokens: int|null, total_tokens: int}}> + * @implements ResponseContract}, finish_reason: string|null}>, usage?: array{prompt_tokens: int, completion_tokens: int|null, total_tokens: int}}> */ final class CreateResponse implements ResponseContract, ResponseHasMetaInformationContract { @@ -41,7 +41,7 @@ private function __construct( /** * 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}, 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}, 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 { @@ -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 afb5150a..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 { @@ -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 { @@ -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 2762550c..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; @@ -37,7 +37,7 @@ private function __construct( /** * 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 { @@ -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 e7816c88..c62b1c65 100644 --- a/src/Responses/Embeddings/CreateResponseEmbedding.php +++ b/src/Responses/Embeddings/CreateResponseEmbedding.php @@ -16,7 +16,7 @@ private function __construct( ) {} /** - * @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 { @@ -28,14 +28,14 @@ public static function from(array $attributes): self } /** - * @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)); } }