From 1745dfb69516647c8628424b960c80d7717df01a Mon Sep 17 00:00:00 2001 From: Jose Garrido <56653670+gauztech@users.noreply.github.com> Date: Sat, 21 Dec 2024 22:49:02 -0500 Subject: [PATCH 01/29] Update README.md It's more readable to name the $response->choices as a $choise instead of a $result --- README.md | 58 +++++++++++++++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 5853b977..f22a7125 100644 --- a/README.md +++ b/README.md @@ -163,11 +163,11 @@ $response->object; // 'text_completion' $response->created; // 1589478378 $response->model; // 'gpt-3.5-turbo-instruct' -foreach ($response->choices as $result) { - $result->text; // '\n\nThis is a test' - $result->index; // 0 - $result->logprobs; // null - $result->finishReason; // 'length' or null +foreach ($response->choices as $choice) { + $choice->text; // '\n\nThis is a test' + $choice->index; // 0 + $choice->logprobs; // null + $choice->finishReason; // 'length' or null } $response->usage->promptTokens; // 5, @@ -217,11 +217,11 @@ $response->object; // 'chat.completion' $response->created; // 1677701073 $response->model; // 'gpt-3.5-turbo-0301' -foreach ($response->choices as $result) { - $result->index; // 0 - $result->message->role; // 'assistant' - $result->message->content; // '\n\nHello there! How can I assist you today?' - $result->finishReason; // 'stop' +foreach ($response->choices as $choice) { + $choice->index; // 0 + $choice->message->role; // 'assistant' + $choice->message->content; // '\n\nHello there! How can I assist you today?' + $choice->finishReason; // 'stop' } $response->usage->promptTokens; // 9, @@ -269,15 +269,15 @@ $response->object; // 'chat.completion' $response->created; // 1677701073 $response->model; // 'gpt-3.5-turbo-0613' -foreach ($response->choices as $result) { - $result->index; // 0 - $result->message->role; // 'assistant' - $result->message->content; // null - $result->message->toolCalls[0]->id; // 'call_123' - $result->message->toolCalls[0]->type; // 'function' - $result->message->toolCalls[0]->function->name; // 'get_current_weather' - $result->message->toolCalls[0]->function->arguments; // "{\n \"location\": \"Boston, MA\"\n}" - $result->finishReason; // 'tool_calls' +foreach ($response->choices as $choice) { + $choice->index; // 0 + $choice->message->role; // 'assistant' + $choice->message->content; // null + $choice->message->toolCalls[0]->id; // 'call_123' + $choice->message->toolCalls[0]->type; // 'function' + $choice->message->toolCalls[0]->function->name; // 'get_current_weather' + $choice->message->toolCalls[0]->function->arguments; // "{\n \"location\": \"Boston, MA\"\n}" + $choice->finishReason; // 'tool_calls' } $response->usage->promptTokens; // 82, @@ -320,13 +320,13 @@ $response->object; // 'chat.completion' $response->created; // 1677701073 $response->model; // 'gpt-3.5-turbo-0613' -foreach ($response->choices as $result) { - $result->index; // 0 - $result->message->role; // 'assistant' - $result->message->content; // null - $result->message->functionCall->name; // 'get_current_weather' - $result->message->functionCall->arguments; // "{\n \"location\": \"Boston, MA\"\n}" - $result->finishReason; // 'function_call' +foreach ($response->choices as $choice) { + $choice->index; // 0 + $choice->message->role; // 'assistant' + $choice->message->content; // null + $choice->message->functionCall->name; // 'get_current_weather' + $choice->message->functionCall->arguments; // "{\n \"location\": \"Boston, MA\"\n}" + $choice->finishReason; // 'function_call' } $response->usage->promptTokens; // 82, @@ -2197,9 +2197,9 @@ $response = $client->edits()->create([ $response->object; // 'edit' $response->created; // 1589478378 -foreach ($response->choices as $result) { - $result->text; // 'What day of the week is it?' - $result->index; // 0 +foreach ($response->choices as $choice) { + $choice->text; // 'What day of the week is it?' + $choice->index; // 0 } $response->usage->promptTokens; // 25, From c43b63f0a5c23338940bc868abf56be91cb5b40f Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sat, 29 Mar 2025 18:28:00 +0000 Subject: [PATCH 02/29] Update README.md --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f22a7125..019fe3c7 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,17 @@

------ -**OpenAI PHP** is a community-maintained PHP API client that allows you to interact with the [Open AI API](https://platform.openai.com/docs/api-reference/introduction). If you or your business relies on this package, it's important to support the developers who have contributed their time and effort to create and maintain this valuable tool: +**OpenAI PHP** is a community-maintained PHP API client that allows you to interact with the [Open AI API](https://platform.openai.com/docs/api-reference/introduction). + +- Follow the creator Nuno Maduro: + - YouTube: **[youtube.com/@nunomaduro](https://www.youtube.com/@nunomaduro)** — Videos every weekday + - Twitch: **[twitch.tv/enunomaduro](https://www.twitch.tv/enunomaduro)** — Streams (almost) every weekday + - Twitter / X: **[x.com/enunomaduro](https://x.com/enunomaduro)** + - LinkedIn: **[linkedin.com/in/nunomaduro](https://www.linkedin.com/in/nunomaduro)** + - Instagram: **[instagram.com/enunomaduro](https://www.instagram.com/enunomaduro)** + - Tiktok: **[tiktok.com/@enunomaduro](https://www.tiktok.com/@enunomaduro)** + +If you or your business relies on this package, it's important to support the developers who have contributed their time and effort to create and maintain this valuable tool: - Nuno Maduro: **[github.com/sponsors/nunomaduro](https://github.com/sponsors/nunomaduro)** - Sandro Gehri: **[github.com/sponsors/gehrisandro](https://github.com/sponsors/gehrisandro)** From dddc2b080b2ccd441c3f0147919fa95df3721552 Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Mon, 31 Mar 2025 08:18:01 -0400 Subject: [PATCH 03/29] fix: update to latest github action versions --- .github/workflows/formats.yml | 4 ++-- .github/workflows/tests.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/formats.yml b/.github/workflows/formats.yml index 993bdf71..e5203c87 100644 --- a/.github/workflows/formats.yml +++ b/.github/workflows/formats.yml @@ -18,10 +18,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Cache dependencies - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: ~/.composer/cache/files key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ef90d46e..d2fa10af 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,10 +17,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Cache dependencies - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: ~/.composer/cache/files key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} From ebf4d8953d6a720a7a0e88c5bcfddd937b5b23fe Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Mon, 31 Mar 2025 08:18:10 -0400 Subject: [PATCH 04/29] feat: opt into Dependabot for GitHub Action health --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..ca79ca5b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly From 5740fce4379f3f4ce34b79edacbb0bd267495506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93scar=20V=2E?= <150821575+ovitores@users.noreply.github.com> Date: Mon, 7 Apr 2025 13:31:02 +0200 Subject: [PATCH 05/29] Fix type definition for responses in ClientFake::addResponses method (#382) * fix type definition for responses in ClientFake::addResponses method * fix lint advises --- src/Testing/ClientFake.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Testing/ClientFake.php b/src/Testing/ClientFake.php index da4d4755..828f2a28 100644 --- a/src/Testing/ClientFake.php +++ b/src/Testing/ClientFake.php @@ -39,7 +39,7 @@ class ClientFake implements ClientContract public function __construct(protected array $responses = []) {} /** - * @param array $responses + * @param array $responses */ public function addResponses(array $responses): void { From 023bd6b59e7f7daa17e3c3b4b73643e47d7387e3 Mon Sep 17 00:00:00 2001 From: martinhoch42 <134395702+martinhoch42@users.noreply.github.com> Date: Tue, 8 Apr 2025 02:27:30 +0200 Subject: [PATCH 06/29] fix: correct content retrieval in HttpTransport by reading full stream. (#343) getContent does not return the content of the body but the remaining content of the body. If you were to OpenAI::factory withHttpClient and i.e. a logging middlewere getContent would return an empty string since the content was already read by the log middleware. See: https://www.php-fig.org/psr/psr-7/ https://stackoverflow.com/questions/30549226/guzzlehttp-how-get-the-body-of-a-response-from-guzzle-6 --- src/Transporters/HttpTransporter.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Transporters/HttpTransporter.php b/src/Transporters/HttpTransporter.php index 2299b832..a2206658 100644 --- a/src/Transporters/HttpTransporter.php +++ b/src/Transporters/HttpTransporter.php @@ -48,7 +48,7 @@ public function requestObject(Payload $payload): Response $response = $this->sendRequest(fn (): \Psr\Http\Message\ResponseInterface => $this->client->sendRequest($request)); - $contents = $response->getBody()->getContents(); + $contents = (string) $response->getBody(); if (str_contains($response->getHeaderLine('Content-Type'), ContentType::TEXT_PLAIN->value)) { return Response::from($contents, $response->getHeaders()); @@ -75,7 +75,7 @@ public function requestContent(Payload $payload): string $response = $this->sendRequest(fn (): \Psr\Http\Message\ResponseInterface => $this->client->sendRequest($request)); - $contents = $response->getBody()->getContents(); + $contents = (string) $response->getBody(); $this->throwIfJsonError($response, $contents); @@ -102,7 +102,7 @@ private function sendRequest(Closure $callable): ResponseInterface return $callable(); } catch (ClientExceptionInterface $clientException) { if ($clientException instanceof ClientException) { - $this->throwIfJsonError($clientException->getResponse(), $clientException->getResponse()->getBody()->getContents()); + $this->throwIfJsonError($clientException->getResponse(), (string) $clientException->getResponse()->getBody()); } throw new TransporterException($clientException); @@ -122,7 +122,7 @@ private function throwIfJsonError(ResponseInterface $response, string|ResponseIn $statusCode = $response->getStatusCode(); if ($contents instanceof ResponseInterface) { - $contents = $contents->getBody()->getContents(); + $contents = (string) $contents->getBody(); } try { From 1afe6a50f26524b75aff7b72ebc3aa9d448e12f5 Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Mon, 7 Apr 2025 20:31:27 -0400 Subject: [PATCH 07/29] build: remove prestissimo (useless in composer v2) (#547) --- .github/workflows/formats.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/formats.yml b/.github/workflows/formats.yml index e5203c87..f3b06a0c 100644 --- a/.github/workflows/formats.yml +++ b/.github/workflows/formats.yml @@ -31,7 +31,6 @@ jobs: with: php-version: ${{ matrix.php }} extensions: dom, mbstring, zip - tools: prestissimo coverage: pcov - name: Install Composer dependencies From a6849a58377b039bfe6670a0c16aca9ade8db4ab Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Mon, 7 Apr 2025 21:56:12 -0400 Subject: [PATCH 08/29] docs: draw attention away from deprecated completions endpoints (#548) * docs: draw attention away from deprecated completions endpoints * chore: add deprecations of edits/finetunes --- README.md | 434 +++++++++++++++++++++++++++--------------------------- 1 file changed, 217 insertions(+), 217 deletions(-) diff --git a/README.md b/README.md index 019fe3c7..1a1839f5 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,8 @@ If you or your business relies on this package, it's important to support the de - [Get Started](#get-started) - [Usage](#usage) - [Models Resource](#models-resource) - - [Completions Resource](#completions-resource) - [Chat Resource](#chat-resource) + - [Completions Resource](#completions-resource) - [Audio Resource](#audio-resource) - [Embeddings Resource](#embeddings-resource) - [Files Resource](#files-resource) @@ -154,60 +154,6 @@ $response->deleted; // true $response->toArray(); // ['id' => 'curie:ft-acmeco-2021-03-03-21-44-20', ...] ``` -### `Completions` Resource - -#### `create` - -Creates a completion for the provided prompt and parameters. - -```php -$response = $client->completions()->create([ - 'model' => 'gpt-3.5-turbo-instruct', - 'prompt' => 'Say this is a test', - 'max_tokens' => 6, - 'temperature' => 0 -]); - -$response->id; // 'cmpl-uqkvlQyYK7bGYrRHQ0eXlWi7' -$response->object; // 'text_completion' -$response->created; // 1589478378 -$response->model; // 'gpt-3.5-turbo-instruct' - -foreach ($response->choices as $choice) { - $choice->text; // '\n\nThis is a test' - $choice->index; // 0 - $choice->logprobs; // null - $choice->finishReason; // 'length' or null -} - -$response->usage->promptTokens; // 5, -$response->usage->completionTokens; // 6, -$response->usage->totalTokens; // 11 - -$response->toArray(); // ['id' => 'cmpl-uqkvlQyYK7bGYrRHQ0eXlWi7', ...] -``` - -#### `create streamed` - -Creates a streamed completion for the provided prompt and parameters. - -```php -$stream = $client->completions()->createStreamed([ - 'model' => 'gpt-3.5-turbo-instruct', - 'prompt' => 'Hi', - 'max_tokens' => 10, - ]); - -foreach($stream as $response){ - $response->choices[0]->text; -} -// 1. iteration => 'I' -// 2. iteration => ' am' -// 3. iteration => ' very' -// 4. iteration => ' excited' -// ... -``` - ### `Chat` Resource #### `create` @@ -387,7 +333,64 @@ foreach($stream as $response){ } ``` - `usage` is always `null` except for the last chunk which contains the token usage statistics for the entire request. +`usage` is always `null` except for the last chunk which contains the token usage statistics for the entire request. + +### `Completions` Resource + +> [!WARNING] +> The `Completions` resource was marked "Legacy" by OpenAI in July 2023. Please use the `Chat` resource instead. + +#### `create` + +Creates a completion for the provided prompt and parameters. + +```php +$response = $client->completions()->create([ + 'model' => 'gpt-3.5-turbo-instruct', + 'prompt' => 'Say this is a test', + 'max_tokens' => 6, + 'temperature' => 0 +]); + +$response->id; // 'cmpl-uqkvlQyYK7bGYrRHQ0eXlWi7' +$response->object; // 'text_completion' +$response->created; // 1589478378 +$response->model; // 'gpt-3.5-turbo-instruct' + +foreach ($response->choices as $choice) { + $choice->text; // '\n\nThis is a test' + $choice->index; // 0 + $choice->logprobs; // null + $choice->finishReason; // 'length' or null +} + +$response->usage->promptTokens; // 5, +$response->usage->completionTokens; // 6, +$response->usage->totalTokens; // 11 + +$response->toArray(); // ['id' => 'cmpl-uqkvlQyYK7bGYrRHQ0eXlWi7', ...] +``` + +#### `create streamed` + +Creates a streamed completion for the provided prompt and parameters. + +```php +$stream = $client->completions()->createStreamed([ + 'model' => 'gpt-3.5-turbo-instruct', + 'prompt' => 'Hi', + 'max_tokens' => 10, + ]); + +foreach($stream as $response){ + $response->choices[0]->text; +} +// 1. iteration => 'I' +// 2. iteration => ' am' +// 3. iteration => ' very' +// 4. iteration => ' excited' +// ... +``` ### `Audio` Resource @@ -722,159 +725,6 @@ $response = $client->fineTuning()->listJobEvents('ftjob-AF1WoRqd3aJAHsqc9NY7iL8F ]); ``` -### `FineTunes` Resource (deprecated) - -#### `create` - -Creates a job that fine-tunes a specified model from a given dataset. - -```php -$response = $client->fineTunes()->create([ - 'training_file' => 'file-ajSREls59WBbvgSzJSVWxMCB', - 'validation_file' => 'file-XjSREls59WBbvgSzJSVWxMCa', - 'model' => 'curie', - 'n_epochs' => 4, - 'batch_size' => null, - 'learning_rate_multiplier' => null, - 'prompt_loss_weight' => 0.01, - 'compute_classification_metrics' => false, - 'classification_n_classes' => null, - 'classification_positive_class' => null, - 'classification_betas' => [], - 'suffix' => null, -]); - -$response->id; // 'ft-AF1WoRqd3aJAHsqc9NY7iL8F' -$response->object; // 'fine-tune' -// ... - -$response->toArray(); // ['id' => 'ft-AF1WoRqd3aJAHsqc9NY7iL8F', ...] -``` - -#### `list` - -List your organization's fine-tuning jobs. - -```php -$response = $client->fineTunes()->list(); - -$response->object; // 'list' - -foreach ($response->data as $result) { - $result->id; // 'ft-AF1WoRqd3aJAHsqc9NY7iL8F' - $result->object; // 'fine-tune' - // ... -} - -$response->toArray(); // ['object' => 'list', 'data' => [...]] -``` - -#### `retrieve` - -Gets info about the fine-tune job. - -```php -$response = $client->fineTunes()->retrieve('ft-AF1WoRqd3aJAHsqc9NY7iL8F'); - -$response->id; // 'ft-AF1WoRqd3aJAHsqc9NY7iL8F' -$response->object; // 'fine-tune' -$response->model; // 'curie' -$response->createdAt; // 1614807352 -$response->fineTunedModel; // 'curie => ft-acmeco-2021-03-03-21-44-20' -$response->organizationId; // 'org-jwe45798ASN82s' -$response->resultFiles; // [ -$response->status; // 'succeeded' -$response->validationFiles; // [ -$response->trainingFiles; // [ -$response->updatedAt; // 1614807865 - -foreach ($response->events as $result) { - $result->object; // 'fine-tune-event' - $result->createdAt; // 1614807352 - $result->level; // 'info' - $result->message; // 'Job enqueued. Waiting for jobs ahead to complete. Queue number => 0.' -} - -$response->hyperparams->batchSize; // 4 -$response->hyperparams->learningRateMultiplier; // 0.1 -$response->hyperparams->nEpochs; // 4 -$response->hyperparams->promptLossWeight; // 0.1 - -foreach ($response->resultFiles as $result) { - $result->id; // 'file-XjGxS3KTG0uNmNOK362iJua3' - $result->object; // 'file' - $result->bytes; // 140 - $result->createdAt; // 1613779657 - $result->filename; // 'mydata.jsonl' - $result->purpose; // 'fine-tune' - $result->status; // 'succeeded' - $result->status_details; // null -} - -foreach ($response->validationFiles as $result) { - $result->id; // 'file-XjGxS3KTG0uNmNOK362iJua3' - // ... -} - -foreach ($response->trainingFiles as $result) { - $result->id; // 'file-XjGxS3KTG0uNmNOK362iJua3' - // ... -} - -$response->toArray(); // ['id' => 'ft-AF1WoRqd3aJAHsqc9NY7iL8F', ...] -``` - -#### `cancel` - -Immediately cancel a fine-tune job. - -```php -$response = $client->fineTunes()->cancel('ft-AF1WoRqd3aJAHsqc9NY7iL8F'); - -$response->id; // 'ft-AF1WoRqd3aJAHsqc9NY7iL8F' -$response->object; // 'fine-tune' -// ... -$response->status; // 'cancelled' -// ... - -$response->toArray(); // ['id' => 'ft-AF1WoRqd3aJAHsqc9NY7iL8F', ...] -``` - -#### `list events` - -Get fine-grained status updates for a fine-tune job. - -```php -$response = $client->fineTunes()->listEvents('ft-AF1WoRqd3aJAHsqc9NY7iL8F'); - -$response->object; // 'list' - -foreach ($response->data as $result) { - $result->object; // 'fine-tune-event' - $result->createdAt; // 1614807352 - // ... -} - -$response->toArray(); // ['object' => 'list', 'data' => [...]] -``` - -#### `list events streamed` - -Get streamed fine-grained status updates for a fine-tune job. - -```php -$stream = $client->fineTunes()->listEventsStreamed('ft-y3OpNlc8B5qBVGCCVsLZsDST'); - -foreach($stream as $response){ - $response->message; -} -// 1. iteration => 'Created fine-tune: ft-y3OpNlc8B5qBVGCCVsLZsDST' -// 2. iteration => 'Fine-tune costs $0.00' -// ... -// xx. iteration => 'Uploaded result file: file-ajLKUCMsFPrT633zqwr0eI4l' -// xx. iteration => 'Fine-tune succeeded' -``` - ### `Moderations` Resource #### `create` @@ -1101,7 +951,6 @@ foreach ($response->data as $result) { $response->toArray(); // ['object' => 'list', ...]] ``` - ### `Threads` Resource #### `create` @@ -1357,7 +1206,6 @@ foreach ($response->data as $result) { $response->toArray(); // ['object' => 'list', ...]] ``` - ### `Threads Runs` Resource #### `create` @@ -1731,7 +1579,6 @@ foreach ($response->data as $result) { $response->toArray(); // ['object' => 'list', ...]] ``` - ### `Batches` Resource #### `create` @@ -1858,7 +1705,6 @@ foreach ($response->data as $result) { $response->toArray(); // ['object' => 'list', ...]] ``` - ### `Vector Stores` Resource #### `create` @@ -1988,7 +1834,6 @@ foreach ($response->data as $result) { $response->toArray(); // ['object' => 'list', ...]] ``` - ### `Vector Store Files` Resource #### `create` @@ -2083,7 +1928,6 @@ foreach ($response->data as $result) { $response->toArray(); // ['object' => 'list', ...]] ``` - ### `Vector Store File Batches` Resource #### `create` @@ -2190,8 +2034,8 @@ $response->toArray(); // ['object' => 'list', ...]] ### `Edits` Resource (deprecated) -> OpenAI has deprecated the Edits API and will stop working by January 4, 2024. -> https://openai.com/blog/gpt-4-api-general-availability#deprecation-of-the-edits-api +> [!WARNING] +> OpenAI has deprecated the Edits API and will stop working by January 4, 2024. https://openai.com/blog/gpt-4-api-general-availability#deprecation-of-the-edits-api #### `create` @@ -2219,6 +2063,162 @@ $response->usage->totalTokens; // 57 $response->toArray(); // ['object' => 'edit', ...] ``` +### `FineTunes` Resource (deprecated) + +> [!WARNING] +> OpenAI has deprecated the FineTunes API and will stop working by January 4, 2024 https://platform.openai.com/docs/deprecations#2023-08-22-fine-tunes-endpoint + +#### `create` + +Creates a job that fine-tunes a specified model from a given dataset. + +```php +$response = $client->fineTunes()->create([ + 'training_file' => 'file-ajSREls59WBbvgSzJSVWxMCB', + 'validation_file' => 'file-XjSREls59WBbvgSzJSVWxMCa', + 'model' => 'curie', + 'n_epochs' => 4, + 'batch_size' => null, + 'learning_rate_multiplier' => null, + 'prompt_loss_weight' => 0.01, + 'compute_classification_metrics' => false, + 'classification_n_classes' => null, + 'classification_positive_class' => null, + 'classification_betas' => [], + 'suffix' => null, +]); + +$response->id; // 'ft-AF1WoRqd3aJAHsqc9NY7iL8F' +$response->object; // 'fine-tune' +// ... + +$response->toArray(); // ['id' => 'ft-AF1WoRqd3aJAHsqc9NY7iL8F', ...] +``` + +#### `list` + +List your organization's fine-tuning jobs. + +```php +$response = $client->fineTunes()->list(); + +$response->object; // 'list' + +foreach ($response->data as $result) { + $result->id; // 'ft-AF1WoRqd3aJAHsqc9NY7iL8F' + $result->object; // 'fine-tune' + // ... +} + +$response->toArray(); // ['object' => 'list', 'data' => [...]] +``` + +#### `retrieve` + +Gets info about the fine-tune job. + +```php +$response = $client->fineTunes()->retrieve('ft-AF1WoRqd3aJAHsqc9NY7iL8F'); + +$response->id; // 'ft-AF1WoRqd3aJAHsqc9NY7iL8F' +$response->object; // 'fine-tune' +$response->model; // 'curie' +$response->createdAt; // 1614807352 +$response->fineTunedModel; // 'curie => ft-acmeco-2021-03-03-21-44-20' +$response->organizationId; // 'org-jwe45798ASN82s' +$response->resultFiles; // [ +$response->status; // 'succeeded' +$response->validationFiles; // [ +$response->trainingFiles; // [ +$response->updatedAt; // 1614807865 + +foreach ($response->events as $result) { + $result->object; // 'fine-tune-event' + $result->createdAt; // 1614807352 + $result->level; // 'info' + $result->message; // 'Job enqueued. Waiting for jobs ahead to complete. Queue number => 0.' +} + +$response->hyperparams->batchSize; // 4 +$response->hyperparams->learningRateMultiplier; // 0.1 +$response->hyperparams->nEpochs; // 4 +$response->hyperparams->promptLossWeight; // 0.1 + +foreach ($response->resultFiles as $result) { + $result->id; // 'file-XjGxS3KTG0uNmNOK362iJua3' + $result->object; // 'file' + $result->bytes; // 140 + $result->createdAt; // 1613779657 + $result->filename; // 'mydata.jsonl' + $result->purpose; // 'fine-tune' + $result->status; // 'succeeded' + $result->status_details; // null +} + +foreach ($response->validationFiles as $result) { + $result->id; // 'file-XjGxS3KTG0uNmNOK362iJua3' + // ... +} + +foreach ($response->trainingFiles as $result) { + $result->id; // 'file-XjGxS3KTG0uNmNOK362iJua3' + // ... +} + +$response->toArray(); // ['id' => 'ft-AF1WoRqd3aJAHsqc9NY7iL8F', ...] +``` + +#### `cancel` + +Immediately cancel a fine-tune job. + +```php +$response = $client->fineTunes()->cancel('ft-AF1WoRqd3aJAHsqc9NY7iL8F'); + +$response->id; // 'ft-AF1WoRqd3aJAHsqc9NY7iL8F' +$response->object; // 'fine-tune' +// ... +$response->status; // 'cancelled' +// ... + +$response->toArray(); // ['id' => 'ft-AF1WoRqd3aJAHsqc9NY7iL8F', ...] +``` + +#### `list events` + +Get fine-grained status updates for a fine-tune job. + +```php +$response = $client->fineTunes()->listEvents('ft-AF1WoRqd3aJAHsqc9NY7iL8F'); + +$response->object; // 'list' + +foreach ($response->data as $result) { + $result->object; // 'fine-tune-event' + $result->createdAt; // 1614807352 + // ... +} + +$response->toArray(); // ['object' => 'list', 'data' => [...]] +``` + +#### `list events streamed` + +Get streamed fine-grained status updates for a fine-tune job. + +```php +$stream = $client->fineTunes()->listEventsStreamed('ft-y3OpNlc8B5qBVGCCVsLZsDST'); + +foreach($stream as $response){ + $response->message; +} +// 1. iteration => 'Created fine-tune: ft-y3OpNlc8B5qBVGCCVsLZsDST' +// 2. iteration => 'Fine-tune costs $0.00' +// ... +// xx. iteration => 'Uploaded result file: file-ajLKUCMsFPrT633zqwr0eI4l' +// xx. iteration => 'Fine-tune succeeded' +``` + ## Meta Information On all response objects you can access the meta information returned by the API via the `meta()` method. From babb3e10589f0e2ccde7ed585809c0056c4dd86b Mon Sep 17 00:00:00 2001 From: Kirsten Mork Date: Tue, 8 Apr 2025 03:35:34 -0700 Subject: [PATCH 09/29] feat: Chat Response - Add logprobs (#533) * Chat Response: Add class for logprobs content * Chat response: add logprobs Only add content for now. * Chat response: add logprobs to CreateResponse Now that we have our classes for logprobs (CreateResponseChoiceLogprobs and CreateResponseChoiceLogbropsContent), let's actually add logprobs in the CreateResponseChoice / CreateResponse objects. Update types, fixtures, tests. * Update readme --- README.md | 1 + src/Resources/Chat.php | 2 +- src/Responses/Chat/CreateResponse.php | 6 +-- src/Responses/Chat/CreateResponseChoice.php | 7 ++- .../Chat/CreateResponseChoiceLogprobs.php | 45 +++++++++++++++++ .../CreateResponseChoiceLogprobsContent.php | 49 +++++++++++++++++++ .../Fixtures/Chat/CreateResponseFixture.php | 1 + tests/Fixtures/Chat.php | 47 ++++++++++++++++++ tests/Responses/Chat/CreateResponseChoice.php | 20 ++++++++ .../Chat/CreateResponseChoiceLogprobs.php | 21 ++++++++ .../CreateResponseChoiceLogprobsContent.php | 19 +++++++ 11 files changed, 212 insertions(+), 6 deletions(-) create mode 100644 src/Responses/Chat/CreateResponseChoiceLogprobs.php create mode 100644 src/Responses/Chat/CreateResponseChoiceLogprobsContent.php create mode 100644 tests/Responses/Chat/CreateResponseChoiceLogprobs.php create mode 100644 tests/Responses/Chat/CreateResponseChoiceLogprobsContent.php diff --git a/README.md b/README.md index 1a1839f5..43894024 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,7 @@ foreach ($response->choices as $choice) { $choice->index; // 0 $choice->message->role; // 'assistant' $choice->message->content; // '\n\nHello there! How can I assist you today?' + $choice->logprobs; // null $choice->finishReason; // 'stop' } diff --git a/src/Resources/Chat.php b/src/Resources/Chat.php index 4d4999f3..2277764b 100644 --- a/src/Resources/Chat.php +++ b/src/Resources/Chat.php @@ -29,7 +29,7 @@ public function create(array $parameters): CreateResponse $payload = Payload::create('chat/completions', $parameters); - /** @var Response}, finish_reason: string|null}>, usage: array{prompt_tokens: int, completion_tokens: int|null, total_tokens: int}}> $response */ + /** @var Response}, logprobs: ?array{content: ?array}>}, finish_reason: string|null}>, usage: array{prompt_tokens: int, completion_tokens: int|null, total_tokens: int}}> $response */ $response = $this->transporter->requestObject($payload); return CreateResponse::from($response->data(), $response->meta()); diff --git a/src/Responses/Chat/CreateResponse.php b/src/Responses/Chat/CreateResponse.php index 3d424e8b..57c1b654 100644 --- a/src/Responses/Chat/CreateResponse.php +++ b/src/Responses/Chat/CreateResponse.php @@ -12,12 +12,12 @@ 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}, 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 { /** - * @use ArrayAccessible}, finish_reason: string|null}>, usage: array{prompt_tokens: int, completion_tokens: int|null, total_tokens: int}}> + * @use ArrayAccessible}, logprobs: ?array{content: ?array}>}, finish_reason: string|null}>, usage: array{prompt_tokens: int, completion_tokens: int|null, total_tokens: int}}> */ use ArrayAccessible; @@ -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}, 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 { diff --git a/src/Responses/Chat/CreateResponseChoice.php b/src/Responses/Chat/CreateResponseChoice.php index d00894d8..eb875f1e 100644 --- a/src/Responses/Chat/CreateResponseChoice.php +++ b/src/Responses/Chat/CreateResponseChoice.php @@ -9,29 +9,32 @@ final class CreateResponseChoice private function __construct( public readonly int $index, public readonly CreateResponseMessage $message, + public readonly ?CreateResponseChoiceLogprobs $logprobs, public readonly ?string $finishReason, ) {} /** - * @param array{index: int, message: array{role: string, content: ?string, function_call: ?array{name: string, arguments: string}, tool_calls: ?array}, finish_reason: string|null} $attributes + * @param array{index: int, message: array{role: string, content: ?string, function_call: ?array{name: string, arguments: string}, tool_calls: ?array}, logprobs: ?array{content: ?array}>}, finish_reason: string|null} $attributes */ public static function from(array $attributes): self { return new self( $attributes['index'], CreateResponseMessage::from($attributes['message']), + $attributes['logprobs'] ? CreateResponseChoiceLogprobs::from($attributes['logprobs']) : null, $attributes['finish_reason'] ?? null, ); } /** - * @return array{index: int, message: array{role: string, content: string|null, function_call?: array{name: string, arguments: string}, tool_calls?: array}, finish_reason: string|null} + * @return array{index: int, message: array{role: string, content: string|null, function_call?: array{name: string, arguments: string}, tool_calls?: array}, logprobs: ?array{content: ?array}>}, finish_reason: string|null} */ public function toArray(): array { return [ 'index' => $this->index, 'message' => $this->message->toArray(), + 'logprobs' => $this->logprobs?->toArray(), 'finish_reason' => $this->finishReason, ]; } diff --git a/src/Responses/Chat/CreateResponseChoiceLogprobs.php b/src/Responses/Chat/CreateResponseChoiceLogprobs.php new file mode 100644 index 00000000..89ac980c --- /dev/null +++ b/src/Responses/Chat/CreateResponseChoiceLogprobs.php @@ -0,0 +1,45 @@ + $content + */ + private function __construct( + public readonly ?array $content, + ) {} + + /** + * @param array{content: ?array}>} $attributes + */ + public static function from(array $attributes): self + { + $content = null; + if (isset($attributes['content'])) { + $content = array_map(fn (array $result): CreateResponseChoiceLogprobsContent => CreateResponseChoiceLogprobsContent::from( + $result + ), $attributes['content']); + } + + return new self( + $content, + ); + } + + /** + * @return array{content: ?array}>} + */ + public function toArray(): array + { + return [ + 'content' => $this->content ? array_map( + static fn (CreateResponseChoiceLogprobsContent $result): array => $result->toArray(), + $this->content, + ) : null, + ]; + } +} diff --git a/src/Responses/Chat/CreateResponseChoiceLogprobsContent.php b/src/Responses/Chat/CreateResponseChoiceLogprobsContent.php new file mode 100644 index 00000000..3b0fab6f --- /dev/null +++ b/src/Responses/Chat/CreateResponseChoiceLogprobsContent.php @@ -0,0 +1,49 @@ + $bytes + */ + private function __construct( + public readonly string $token, + public readonly float $logprob, + public readonly ?array $bytes, + ) {} + + /** + * @param array{ + * token: string, + * logprob: float, + * bytes: ?array + * } $attributes + */ + public static function from(array $attributes): self + { + return new self( + $attributes['token'], + $attributes['logprob'], + $attributes['bytes'], + ); + } + + /** + * @return array{ + * token: string, + * logprob: float, + * bytes: ?array + * } + */ + public function toArray(): array + { + return [ + 'token' => $this->token, + 'logprob' => $this->logprob, + 'bytes' => $this->bytes, + ]; + } +} diff --git a/src/Testing/Responses/Fixtures/Chat/CreateResponseFixture.php b/src/Testing/Responses/Fixtures/Chat/CreateResponseFixture.php index 43880166..5b226c7d 100644 --- a/src/Testing/Responses/Fixtures/Chat/CreateResponseFixture.php +++ b/src/Testing/Responses/Fixtures/Chat/CreateResponseFixture.php @@ -19,6 +19,7 @@ final class CreateResponseFixture 'function_call' => null, 'tool_calls' => [], ], + 'logprobs' => null, 'finish_reason' => 'stop', ], ], diff --git a/tests/Fixtures/Chat.php b/tests/Fixtures/Chat.php index 9d9c5ed5..a15795df 100644 --- a/tests/Fixtures/Chat.php +++ b/tests/Fixtures/Chat.php @@ -17,6 +17,7 @@ function chatCompletion(): array 'role' => 'assistant', 'content' => "\n\nHello there, how may I assist you today?", ], + 'logprobs' => null, 'finish_reason' => 'stop', ], ], @@ -36,6 +37,48 @@ function chatCompletion(): array ]; } +/** + * @return array + */ +function chatCompletionWithLogprobs(): array +{ + return [ + 'id' => 'chatcmpl-123', + 'object' => 'chat.completion', + 'created' => 1677652288, + 'model' => 'gpt-3.5-turbo', + 'choices' => [ + [ + 'index' => 0, + 'message' => [ + 'role' => 'assistant', + 'content' => 'Hello!', + ], + 'logprobs' => [ + 'content' => [ + [ + 'token' => 'Hello', + 'logprob' => 0.0, + 'bytes' => [72, 101, 108, 108, 111], + ], + [ + 'token' => '!', + 'logprob' => -0.0005715019651688635, + 'bytes' => [33], + ], + ], + ], + 'finish_reason' => 'stop', + ], + ], + 'usage' => [ + 'prompt_tokens' => 18, + 'completion_tokens' => 3, + 'total_tokens' => 21, + ], + ]; +} + /** * @return array */ @@ -54,6 +97,7 @@ function chatCompletionWithSystemFingerprint(): array 'role' => 'assistant', 'content' => "\n\nHello there, how may I assist you today?", ], + 'logprobs' => null, 'finish_reason' => 'stop', ], ], @@ -86,6 +130,7 @@ function chatCompletionWithFunction(): array 'arguments' => "{\n \"location\": \"Boston, MA\"\n}", ], ], + 'logprobs' => null, 'finish_reason' => 'function_call', ], ], @@ -124,6 +169,7 @@ function chatCompletionWithToolCalls(): array ], ], ], + 'logprobs' => null, 'finish_reason' => 'tool_calls', ], ], @@ -166,6 +212,7 @@ function chatCompletionFromVision(): array 'role' => 'assistant', 'content' => 'The image shows a beautiful, tranquil natural landscape. A wooden boardwalk path stretches', ], + 'logprobs' => null, ], ], 'usage' => [ diff --git a/tests/Responses/Chat/CreateResponseChoice.php b/tests/Responses/Chat/CreateResponseChoice.php index 03481cb7..e5a129a5 100644 --- a/tests/Responses/Chat/CreateResponseChoice.php +++ b/tests/Responses/Chat/CreateResponseChoice.php @@ -1,6 +1,7 @@ index->toBe(0) ->message->toBeInstanceOf(CreateResponseMessage::class) + ->logprobs->toBeNull() + ->finishReason->toBeIn(['stop', null]); +}); + +test('from with logprobs', function () { + $result = CreateResponseChoice::from(chatCompletionWithLogprobs()['choices'][0]); + + expect($result) + ->index->toBe(0) + ->message->toBeInstanceOf(CreateResponseMessage::class) + ->logprobs->toBeInstanceOf(CreateResponseChoiceLogprobs::class) ->finishReason->toBeIn(['stop', null]); }); @@ -18,6 +30,7 @@ expect($result) ->index->toBe(0) ->message->toBeInstanceOf(CreateResponseMessage::class) + ->logprobs->toBeNull() ->finishReason->toBeNull(); }); @@ -27,3 +40,10 @@ expect($result->toArray()) ->toBe(chatCompletion()['choices'][0]); }); + +test('to array with logprobs', function () { + $result = CreateResponseChoice::from(chatCompletionWithLogprobs()['choices'][0]); + + expect($result->toArray()) + ->toBe(chatCompletionWithLogprobs()['choices'][0]); +}); diff --git a/tests/Responses/Chat/CreateResponseChoiceLogprobs.php b/tests/Responses/Chat/CreateResponseChoiceLogprobs.php new file mode 100644 index 00000000..4bbf4a79 --- /dev/null +++ b/tests/Responses/Chat/CreateResponseChoiceLogprobs.php @@ -0,0 +1,21 @@ +toBeInstanceOf(CreateResponseChoiceLogprobs::class) + ->content->toBeArray() + ->content->toHaveCount(2) + ->content->each->toBeInstanceOf(CreateResponseChoiceLogprobsContent::class); +}); + +test('to array', function () { + $result = CreateResponseChoiceLogprobs::from(chatCompletionWithLogprobs()['choices'][0]['logprobs']); + + expect($result->toArray()) + ->toBe(chatCompletionWithLogprobs()['choices'][0]['logprobs']); +}); diff --git a/tests/Responses/Chat/CreateResponseChoiceLogprobsContent.php b/tests/Responses/Chat/CreateResponseChoiceLogprobsContent.php new file mode 100644 index 00000000..1545e9c1 --- /dev/null +++ b/tests/Responses/Chat/CreateResponseChoiceLogprobsContent.php @@ -0,0 +1,19 @@ +token->toBe('Hello') + ->logprob->toBe(0.0) + ->bytes->toBe([72, 101, 108, 108, 111]); +}); + +test('to array', function () { + $result = CreateResponseChoiceLogprobsContent::from(chatCompletionWithLogprobs()['choices'][0]['logprobs']['content'][0]); + + expect($result->toArray()) + ->toBe(chatCompletionWithLogprobs()['choices'][0]['logprobs']['content'][0]); +}); From 6be1fb8844201ee307367b54a8cd036b3da30613 Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Tue, 8 Apr 2025 11:36:55 -0400 Subject: [PATCH 10/29] fix: correct completion endpoint when logprobs missing (#550) --- src/Responses/Completions/CreateResponseChoice.php | 4 +++- tests/Responses/Completions/CreateResponseChoice.php | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Responses/Completions/CreateResponseChoice.php b/src/Responses/Completions/CreateResponseChoice.php index d84c06e3..8a56af8c 100644 --- a/src/Responses/Completions/CreateResponseChoice.php +++ b/src/Responses/Completions/CreateResponseChoice.php @@ -21,7 +21,9 @@ public static function from(array $attributes): self return new self( $attributes['text'], $attributes['index'], - $attributes['logprobs'] ? CreateResponseChoiceLogprobs::from($attributes['logprobs']) : null, + isset($attributes['logprobs']) + ? CreateResponseChoiceLogprobs::from($attributes['logprobs']) + : null, $attributes['finish_reason'], ); } diff --git a/tests/Responses/Completions/CreateResponseChoice.php b/tests/Responses/Completions/CreateResponseChoice.php index afe6224a..62f2ff48 100644 --- a/tests/Responses/Completions/CreateResponseChoice.php +++ b/tests/Responses/Completions/CreateResponseChoice.php @@ -13,6 +13,18 @@ ->finishReason->toBeIn(['length', null]); }); +test('from with missing logprobs', function () { + $payload = completion()['choices'][0]; + unset($payload['logprobs']); + $result = CreateResponseChoice::from($payload); + + expect($result) + ->text->toBe("el, she elaborates more on the Corruptor's role, suggesting K") + ->index->toBe(0) + ->logprobs->toBeNull() + ->finishReason->toBeIn(['length', null]); +}); + test('to array', function () { $result = CreateResponseChoice::from(completion()['choices'][0]); From dadf819fafb382397e6aa7fa0bd9be7c845f6cd2 Mon Sep 17 00:00:00 2001 From: Gareth Davis <127886287+gareth-civia@users.noreply.github.com> Date: Tue, 8 Apr 2025 19:38:24 -0400 Subject: [PATCH 11/29] fix: add ResponseHasMetaInformationContract contract to ThreadRunStepResponse (#523) --- src/Responses/Threads/Runs/Steps/ThreadRunStepResponse.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Responses/Threads/Runs/Steps/ThreadRunStepResponse.php b/src/Responses/Threads/Runs/Steps/ThreadRunStepResponse.php index 8f3689d6..df25e7d0 100644 --- a/src/Responses/Threads/Runs/Steps/ThreadRunStepResponse.php +++ b/src/Responses/Threads/Runs/Steps/ThreadRunStepResponse.php @@ -5,6 +5,7 @@ namespace OpenAI\Responses\Threads\Runs\Steps; use OpenAI\Contracts\ResponseContract; +use OpenAI\Contracts\ResponseHasMetaInformationContract; use OpenAI\Responses\Concerns\ArrayAccessible; use OpenAI\Responses\Concerns\HasMetaInformation; use OpenAI\Responses\Meta\MetaInformation; @@ -14,7 +15,7 @@ /** * @implements ResponseContract}}|array{id: string, type: 'file_search', file_search: array}|array{id: ?string, type: 'function', function: array{name: ?string, arguments: string, output: ?string}}>}|array{type: 'message_creation', message_creation: array{message_id: string}}, last_error: ?array{code: string, message: string}, expires_at: ?int, cancelled_at: ?int, failed_at: ?int, completed_at: ?int, metadata?: array, usage: ?array{prompt_tokens: int, completion_tokens: int, total_tokens: int}}> */ -final class ThreadRunStepResponse implements ResponseContract +final class ThreadRunStepResponse implements ResponseContract, ResponseHasMetaInformationContract { /** * @use ArrayAccessible}}|array{id: string, type: 'file_search', file_search: array}|array{id: ?string, type: 'function', function: array{name: ?string, arguments: string, output: ?string}}>}|array{type: 'message_creation', message_creation: array{message_id: string}}, last_error: ?array{code: string, message: string}, expires_at: ?int, cancelled_at: ?int, failed_at: ?int, completed_at: ?int, metadata?: array, usage: ?array{prompt_tokens: int, completion_tokens: int, total_tokens: int}}> From 2ca39cc5e5829f5d826181a2c511fe35bfda93b3 Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Thu, 10 Apr 2025 08:00:20 -0400 Subject: [PATCH 12/29] feat: support 'attributes' on vector store files (#551) * feat: support 'attributes' on vector store files * test: add assertion for attributes * test: add assertion to confirm missing attributes work * test: leave last_error testcase unchanged --- src/Resources/VectorStoresFileBatches.php | 2 +- src/Resources/VectorStoresFiles.php | 6 +++--- .../Files/VectorStoreFileListResponse.php | 8 ++++---- .../VectorStores/Files/VectorStoreFileResponse.php | 12 +++++++++--- .../Files/VectorStoreFileListResponseFixture.php | 1 + .../Files/VectorStoreFileResponseFixture.php | 1 + .../VectorStores/VectorStoreListResponseFixture.php | 2 ++ tests/Fixtures/VectorStoreFile.php | 3 +++ .../VectorStores/Files/VectorStoreFileResponse.php | 10 ++++++++++ 9 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/Resources/VectorStoresFileBatches.php b/src/Resources/VectorStoresFileBatches.php index 946a6ca7..e35cebfa 100644 --- a/src/Resources/VectorStoresFileBatches.php +++ b/src/Resources/VectorStoresFileBatches.php @@ -57,7 +57,7 @@ public function listFiles(string $vectorStoreId, string $fileBatchId, array $par { $payload = Payload::list("vector_stores/$vectorStoreId/file_batches/$fileBatchId/files", $parameters); - /** @var Response, first_id: ?string, last_id: ?string, has_more: bool}> $response */ + /** @var Response, last_error: ?array{code: string, message: string}, chunking_strategy: array{type: 'static', static: array{max_chunk_size_tokens: int, chunk_overlap_tokens: int}}|array{type: 'other'}}>, first_id: ?string, last_id: ?string, has_more: bool}> $response */ $response = $this->transporter->requestObject($payload); return VectorStoreFileListResponse::from($response->data(), $response->meta()); diff --git a/src/Resources/VectorStoresFiles.php b/src/Resources/VectorStoresFiles.php index 2b58912c..4b44b8dd 100644 --- a/src/Resources/VectorStoresFiles.php +++ b/src/Resources/VectorStoresFiles.php @@ -26,7 +26,7 @@ public function create(string $vectorStoreId, array $parameters): VectorStoreFil { $payload = Payload::create("vector_stores/$vectorStoreId/files", $parameters); - /** @var Response $response */ + /** @var Response, last_error: ?array{code: string, message: string}, chunking_strategy: array{type: 'static', static: array{max_chunk_size_tokens: int, chunk_overlap_tokens: int}}|array{type: 'other'}}> $response */ $response = $this->transporter->requestObject($payload); return VectorStoreFileResponse::from($response->data(), $response->meta()); @@ -43,7 +43,7 @@ public function list(string $vectorStoreId, array $parameters = []): VectorStore { $payload = Payload::list("vector_stores/$vectorStoreId/files", $parameters); - /** @var Response, first_id: ?string, last_id: ?string, has_more: bool}> $response */ + /** @var Response, last_error: ?array{code: string, message: string}, chunking_strategy: array{type: 'static', static: array{max_chunk_size_tokens: int, chunk_overlap_tokens: int}}|array{type: 'other'}}>, first_id: ?string, last_id: ?string, has_more: bool}> $response */ $response = $this->transporter->requestObject($payload); return VectorStoreFileListResponse::from($response->data(), $response->meta()); @@ -58,7 +58,7 @@ public function retrieve(string $vectorStoreId, string $fileId): VectorStoreFile { $payload = Payload::retrieve("vector_stores/$vectorStoreId/files", $fileId); - /** @var Response $response */ + /** @var Response, last_error: ?array{code: string, message: string}, chunking_strategy: array{type: 'static', static: array{max_chunk_size_tokens: int, chunk_overlap_tokens: int}}|array{type: 'other'}}> $response */ $response = $this->transporter->requestObject($payload); return VectorStoreFileResponse::from($response->data(), $response->meta()); diff --git a/src/Responses/VectorStores/Files/VectorStoreFileListResponse.php b/src/Responses/VectorStores/Files/VectorStoreFileListResponse.php index 5e6d6e26..bdc764e2 100644 --- a/src/Responses/VectorStores/Files/VectorStoreFileListResponse.php +++ b/src/Responses/VectorStores/Files/VectorStoreFileListResponse.php @@ -12,12 +12,12 @@ use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @implements ResponseContract, first_id: ?string, last_id: ?string, has_more: bool}> + * @implements ResponseContract, last_error: ?array{code: string, message: string}}>, first_id: ?string, last_id: ?string, has_more: bool}> */ final class VectorStoreFileListResponse implements ResponseContract, ResponseHasMetaInformationContract { /** - * @use ArrayAccessible, first_id: ?string, last_id: ?string, has_more: bool}> + * @use ArrayAccessible, last_error: ?array{code: string, message: string}}>, first_id: ?string, last_id: ?string, has_more: bool}> */ use ArrayAccessible; @@ -39,7 +39,7 @@ private function __construct( /** * Acts as static factory, and returns a new Response instance. * - * @param array{object: string, data: array, first_id: ?string, last_id: ?string, has_more: bool} $attributes + * @param array{object: string, data: array, last_error: ?array{code: string, message: string}, chunking_strategy: array{type: 'static', static: array{max_chunk_size_tokens: int, chunk_overlap_tokens: int}}|array{type: 'other'}}>, first_id: ?string, last_id: ?string, has_more: bool} $attributes */ public static function from(array $attributes, MetaInformation $meta): self { @@ -61,7 +61,7 @@ public static function from(array $attributes, MetaInformation $meta): self /** * {@inheritDoc} * - * @return array{object: string, data: array, first_id: string|null, last_id: string|null, has_more: bool} + * @return array{object: string, data: array, last_error: array{code: string, message: string}|null}>, first_id: string|null, last_id: string|null, has_more: bool} */ public function toArray(): array { diff --git a/src/Responses/VectorStores/Files/VectorStoreFileResponse.php b/src/Responses/VectorStores/Files/VectorStoreFileResponse.php index 9ef3666b..cf78c934 100644 --- a/src/Responses/VectorStores/Files/VectorStoreFileResponse.php +++ b/src/Responses/VectorStores/Files/VectorStoreFileResponse.php @@ -12,18 +12,21 @@ use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @implements ResponseContract + * @implements ResponseContract, last_error: ?array{code: string, message: string}, chunking_strategy: array{type: 'static', static: array{max_chunk_size_tokens: int, chunk_overlap_tokens: int}}|array{type: 'other'}}> */ final class VectorStoreFileResponse implements ResponseContract, ResponseHasMetaInformationContract { /** - * @use ArrayAccessible + * @use ArrayAccessible, last_error: ?array{code: string, message: string}, chunking_strategy: array{type: 'static', static: array{max_chunk_size_tokens: int, chunk_overlap_tokens: int}}|array{type: 'other'}}> */ use ArrayAccessible; use Fakeable; use HasMetaInformation; + /** + * @param array $attributes + */ private function __construct( public readonly string $id, public readonly string $object, @@ -31,6 +34,7 @@ private function __construct( public readonly int $createdAt, public readonly string $vectorStoreId, public readonly string $status, + public readonly array $attributes, public readonly ?VectorStoreFileResponseLastError $lastError, public readonly VectorStoreFileResponseChunkingStrategyStatic|VectorStoreFileResponseChunkingStrategyOther $chunkingStrategy, private readonly MetaInformation $meta, @@ -39,7 +43,7 @@ private function __construct( /** * Acts as static factory, and returns a new Response instance. * - * @param array{id: string, object: string, usage_bytes: int, created_at: int, vector_store_id: string, status: string, last_error: ?array{code: string, message: string}, chunking_strategy: array{type: 'static', static: array{max_chunk_size_tokens: int, chunk_overlap_tokens: int}}|array{type: 'other'}} $attributes + * @param array{id: string, object: string, usage_bytes: int, created_at: int, vector_store_id: string, status: string, attributes: ?array, last_error: ?array{code: string, message: string}, chunking_strategy: array{type: 'static', static: array{max_chunk_size_tokens: int, chunk_overlap_tokens: int}}|array{type: 'other'}} $attributes */ public static function from(array $attributes, MetaInformation $meta): self { @@ -50,6 +54,7 @@ public static function from(array $attributes, MetaInformation $meta): self $attributes['created_at'], $attributes['vector_store_id'], $attributes['status'], + $attributes['attributes'] ?? [], isset($attributes['last_error']) ? VectorStoreFileResponseLastError::from($attributes['last_error']) : null, $attributes['chunking_strategy']['type'] === 'static' ? VectorStoreFileResponseChunkingStrategyStatic::from($attributes['chunking_strategy']) : VectorStoreFileResponseChunkingStrategyOther::from($attributes['chunking_strategy']), $meta, @@ -68,6 +73,7 @@ public function toArray(): array 'created_at' => $this->createdAt, 'vector_store_id' => $this->vectorStoreId, 'status' => $this->status, + 'attributes' => $this->attributes, 'last_error' => $this->lastError instanceof VectorStoreFileResponseLastError ? $this->lastError->toArray() : null, 'chunking_strategy' => $this->chunkingStrategy->toArray(), ]; diff --git a/src/Testing/Responses/Fixtures/VectorStores/Files/VectorStoreFileListResponseFixture.php b/src/Testing/Responses/Fixtures/VectorStores/Files/VectorStoreFileListResponseFixture.php index b585f401..cf00d06c 100644 --- a/src/Testing/Responses/Fixtures/VectorStores/Files/VectorStoreFileListResponseFixture.php +++ b/src/Testing/Responses/Fixtures/VectorStores/Files/VectorStoreFileListResponseFixture.php @@ -14,6 +14,7 @@ final class VectorStoreFileListResponseFixture 'created_at' => 1_715_956_697, 'vector_store_id' => 'vs_xds05V7ep0QMGI5JmYnWsJwb', 'status' => 'completed', + 'attributes' => [], 'last_error' => null, ], ], diff --git a/src/Testing/Responses/Fixtures/VectorStores/Files/VectorStoreFileResponseFixture.php b/src/Testing/Responses/Fixtures/VectorStores/Files/VectorStoreFileResponseFixture.php index df2e57bc..97179f12 100644 --- a/src/Testing/Responses/Fixtures/VectorStores/Files/VectorStoreFileResponseFixture.php +++ b/src/Testing/Responses/Fixtures/VectorStores/Files/VectorStoreFileResponseFixture.php @@ -11,6 +11,7 @@ final class VectorStoreFileResponseFixture 'created_at' => 1_715_956_697, 'vector_store_id' => 'vs_xds05V7ep0QMGI5JmYnWsJwb', 'status' => 'completed', + 'attributes' => [], 'last_error' => null, ]; } diff --git a/src/Testing/Responses/Fixtures/VectorStores/VectorStoreListResponseFixture.php b/src/Testing/Responses/Fixtures/VectorStores/VectorStoreListResponseFixture.php index 0c532f4e..8bcb5eb5 100644 --- a/src/Testing/Responses/Fixtures/VectorStores/VectorStoreListResponseFixture.php +++ b/src/Testing/Responses/Fixtures/VectorStores/VectorStoreListResponseFixture.php @@ -12,6 +12,7 @@ final class VectorStoreListResponseFixture 'object' => 'vector_store', 'name' => 'Product Knowledge Base', 'status' => 'completed', + 'attributes' => [], 'usage_bytes' => 29882, 'created_at' => 1_715_953_317, 'file_counts' => [ @@ -31,6 +32,7 @@ final class VectorStoreListResponseFixture 'object' => 'vector_store', 'name' => null, 'status' => 'completed', + 'attributes' => [], 'usage_bytes' => 0, 'created_at' => 1_710_869_420, 'file_counts' => [ diff --git a/tests/Fixtures/VectorStoreFile.php b/tests/Fixtures/VectorStoreFile.php index e22917e1..66f312a2 100644 --- a/tests/Fixtures/VectorStoreFile.php +++ b/tests/Fixtures/VectorStoreFile.php @@ -12,6 +12,9 @@ function vectorStoreFileResource(): array 'created_at' => 1715956697, 'vector_store_id' => 'vs_xds05V7ep0QMGI5JmYnWsJwb', 'status' => 'completed', + 'attributes' => [ + 'foo' => 'bar', + ], 'last_error' => null, 'chunking_strategy' => [ 'type' => 'static', diff --git a/tests/Responses/VectorStores/Files/VectorStoreFileResponse.php b/tests/Responses/VectorStores/Files/VectorStoreFileResponse.php index ed5aef57..08ecbe7b 100644 --- a/tests/Responses/VectorStores/Files/VectorStoreFileResponse.php +++ b/tests/Responses/VectorStores/Files/VectorStoreFileResponse.php @@ -13,6 +13,7 @@ ->createdAt->toBe(1715956697) ->vectorStoreId->toBe('vs_xds05V7ep0QMGI5JmYnWsJwb') ->status->toBe('completed') + ->attributes->toBe(['foo' => 'bar']) ->lastError->toBeNull() ->chunkingStrategy->toBeInstanceOf(VectorStoreFileResponseChunkingStrategyStatic::class) ->chunkingStrategy->type->toBe('static') @@ -20,6 +21,15 @@ ->chunkingStrategy->chunkOverlapTokens->toBe(400); }); +test('from while missing attributes', function () { + $payload = vectorStoreFileResource(); + unset($payload['attributes']); + $result = VectorStoreFileResponse::from($payload, meta()); + + expect($result) + ->attributes->toBe([]); +}); + test('as array accessible', function () { $result = VectorStoreFileResponse::from(vectorStoreFileResource(), meta()); From 640306053894c0ae7a2da85c895eb2e4542a9009 Mon Sep 17 00:00:00 2001 From: EJLin <42922266+TyperEJ@users.noreply.github.com> Date: Fri, 11 Apr 2025 05:42:44 +0800 Subject: [PATCH 13/29] feat: add OpenAI compatibility support for Google Gemini (#502) * feat: add openai compatibility support for google gemini * fix: coding style checks * fix: phpstan analyse --- src/Responses/Chat/CreateResponse.php | 14 ++-- src/Responses/Chat/CreateStreamedResponse.php | 19 ++--- src/Responses/Embeddings/CreateResponse.php | 16 ++-- .../Embeddings/CreateResponseEmbedding.php | 12 +-- 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, 205 insertions(+), 33 deletions(-) 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()); From ad7743dd174e513ece48046e78116d5ccb0d7334 Mon Sep 17 00:00:00 2001 From: EJLin <42922266+TyperEJ@users.noreply.github.com> Date: Fri, 11 Apr 2025 18:21:04 +0800 Subject: [PATCH 14/29] fix: chat completion choices to allow responses without logprobs field (#554) --- src/Responses/Chat/CreateResponseChoice.php | 4 +-- tests/Fixtures/Chat.php | 36 +++++++++++++++++++ tests/Responses/Chat/CreateResponseChoice.php | 10 ++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/Responses/Chat/CreateResponseChoice.php b/src/Responses/Chat/CreateResponseChoice.php index eb875f1e..a592d9de 100644 --- a/src/Responses/Chat/CreateResponseChoice.php +++ b/src/Responses/Chat/CreateResponseChoice.php @@ -14,14 +14,14 @@ private function __construct( ) {} /** - * @param array{index: int, message: array{role: string, content: ?string, function_call: ?array{name: string, arguments: string}, tool_calls: ?array}, logprobs: ?array{content: ?array}>}, finish_reason: string|null} $attributes + * @param array{index: int, message: array{role: string, content: ?string, function_call: ?array{name: string, arguments: string}, tool_calls: ?array}, logprobs?: ?array{content: ?array}>}, finish_reason: string|null} $attributes */ public static function from(array $attributes): self { return new self( $attributes['index'], CreateResponseMessage::from($attributes['message']), - $attributes['logprobs'] ? CreateResponseChoiceLogprobs::from($attributes['logprobs']) : null, + isset($attributes['logprobs']) ? CreateResponseChoiceLogprobs::from($attributes['logprobs']) : null, $attributes['finish_reason'] ?? null, ); } diff --git a/tests/Fixtures/Chat.php b/tests/Fixtures/Chat.php index f19b14e6..aaae5894 100644 --- a/tests/Fixtures/Chat.php +++ b/tests/Fixtures/Chat.php @@ -95,6 +95,42 @@ function chatCompletionWithoutUsage(): array ]; } +/** + * @return array + */ +function chatCompletionWithoutLogprobs(): 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', + ], + ], + '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 */ diff --git a/tests/Responses/Chat/CreateResponseChoice.php b/tests/Responses/Chat/CreateResponseChoice.php index e5a129a5..af255e85 100644 --- a/tests/Responses/Chat/CreateResponseChoice.php +++ b/tests/Responses/Chat/CreateResponseChoice.php @@ -14,6 +14,16 @@ ->finishReason->toBeIn(['stop', null]); }); +test('from without logprobs', function () { + $result = CreateResponseChoice::from(chatCompletionWithoutLogprobs()['choices'][0]); + + expect($result) + ->index->toBe(0) + ->message->toBeInstanceOf(CreateResponseMessage::class) + ->logprobs->toBeNull() + ->finishReason->toBeIn(['stop', null]); +}); + test('from with logprobs', function () { $result = CreateResponseChoice::from(chatCompletionWithLogprobs()['choices'][0]); From 54a2c0fd0796e83e1dbd881d0df62a0841e31e5a Mon Sep 17 00:00:00 2001 From: Kyle Nash Date: Fri, 11 Apr 2025 11:40:52 +0100 Subject: [PATCH 15/29] test: boost test coverage for assistant streaming & introduce Fakeable ThreadRunStreamResponse (#444) --- src/Responses/Batches/BatchResponse.php | 2 +- .../Threads/Runs/ThreadRunStreamResponse.php | 3 + .../Batches/BatchListResponseFixture.php | 10 +-- .../Fixtures/Batches/BatchResponseFixture.php | 2 +- .../Runs/ThreadRunStreamResponseFixture.txt | 41 ++++++++++++ .../ThreadRunStreamInvalidEventResponse.txt | 2 + .../ThreadRunStreamMessageDeltaResponse.txt | 2 + .../ThreadRunStreamMessageResponse.txt | 2 + .../Streams/ThreadRunStreamResponse.txt | 41 ++++++++++++ .../ThreadRunStreamRunStepDeltaResponse.txt | 2 + .../ThreadRunStreamStepRunCreatedResponse.txt | 2 + .../ThreadRunStreamThreadCreatedResponse.txt | 2 + tests/Fixtures/ThreadRun.php | 48 ++++++++++++++ .../Threads/Runs/ThreadRunStreamResponse.php | 65 +++++++++++++++++++ 14 files changed, 217 insertions(+), 7 deletions(-) create mode 100644 src/Testing/Responses/Fixtures/Threads/Runs/ThreadRunStreamResponseFixture.txt create mode 100644 tests/Fixtures/Streams/ThreadRunStreamInvalidEventResponse.txt create mode 100644 tests/Fixtures/Streams/ThreadRunStreamMessageDeltaResponse.txt create mode 100644 tests/Fixtures/Streams/ThreadRunStreamMessageResponse.txt create mode 100644 tests/Fixtures/Streams/ThreadRunStreamResponse.txt create mode 100644 tests/Fixtures/Streams/ThreadRunStreamRunStepDeltaResponse.txt create mode 100644 tests/Fixtures/Streams/ThreadRunStreamStepRunCreatedResponse.txt create mode 100644 tests/Fixtures/Streams/ThreadRunStreamThreadCreatedResponse.txt create mode 100644 tests/Responses/Threads/Runs/ThreadRunStreamResponse.php diff --git a/src/Responses/Batches/BatchResponse.php b/src/Responses/Batches/BatchResponse.php index 86c269f8..17c54895 100644 --- a/src/Responses/Batches/BatchResponse.php +++ b/src/Responses/Batches/BatchResponse.php @@ -25,7 +25,7 @@ final class BatchResponse implements ResponseContract, ResponseHasMetaInformatio use HasMetaInformation; /** - * @param array $metadata + * @param array|null $metadata */ private function __construct( public string $id, diff --git a/src/Responses/Threads/Runs/ThreadRunStreamResponse.php b/src/Responses/Threads/Runs/ThreadRunStreamResponse.php index 27a884b5..ab450cc6 100644 --- a/src/Responses/Threads/Runs/ThreadRunStreamResponse.php +++ b/src/Responses/Threads/Runs/ThreadRunStreamResponse.php @@ -10,6 +10,7 @@ use OpenAI\Responses\Threads\Runs\Steps\Delta\ThreadRunStepDeltaResponse; use OpenAI\Responses\Threads\Runs\Steps\ThreadRunStepResponse; use OpenAI\Responses\Threads\ThreadResponse; +use OpenAI\Testing\Responses\Concerns\FakeableForStreamedResponse; /** * @implements ResponseContract}> @@ -21,6 +22,8 @@ class ThreadRunStreamResponse implements ResponseContract */ use ArrayAccessible; + use FakeableForStreamedResponse; + private function __construct( public readonly string $event, public readonly ThreadResponse|ThreadRunResponse|ThreadRunStepResponse|ThreadRunStepDeltaResponse|ThreadMessageResponse|ThreadMessageDeltaResponse $response, diff --git a/src/Testing/Responses/Fixtures/Batches/BatchListResponseFixture.php b/src/Testing/Responses/Fixtures/Batches/BatchListResponseFixture.php index e578c66c..e58aaf89 100644 --- a/src/Testing/Responses/Fixtures/Batches/BatchListResponseFixture.php +++ b/src/Testing/Responses/Fixtures/Batches/BatchListResponseFixture.php @@ -17,11 +17,11 @@ final class BatchListResponseFixture 'status' => 'completed', 'output_file_id' => 'file-cvaTdG', 'error_file_id' => 'file-HOWS94', - 'created_at' => 1711471533, - 'in_progress_at' => 1711471538, - 'expires_at' => 1711557933, - 'finalizing_at' => 1711493133, - 'completed_at' => 1711493163, + 'created_at' => 1_711_471_533, + 'in_progress_at' => 1_711_471_538, + 'expires_at' => 1_711_557_933, + 'finalizing_at' => 1_711_493_133, + 'completed_at' => 1_711_493_163, 'failed_at' => null, 'expired_at' => null, 'cancelling_at' => null, diff --git a/src/Testing/Responses/Fixtures/Batches/BatchResponseFixture.php b/src/Testing/Responses/Fixtures/Batches/BatchResponseFixture.php index 7a51f543..97d55591 100644 --- a/src/Testing/Responses/Fixtures/Batches/BatchResponseFixture.php +++ b/src/Testing/Responses/Fixtures/Batches/BatchResponseFixture.php @@ -14,7 +14,7 @@ final class BatchResponseFixture 'status' => 'validating', 'output_file_id' => null, 'error_file_id' => null, - 'created_at' => 1711471533, + 'created_at' => 1_711_471_533, 'in_progress_at' => null, 'expires_at' => null, 'finalizing_at' => null, diff --git a/src/Testing/Responses/Fixtures/Threads/Runs/ThreadRunStreamResponseFixture.txt b/src/Testing/Responses/Fixtures/Threads/Runs/ThreadRunStreamResponseFixture.txt new file mode 100644 index 00000000..74781b70 --- /dev/null +++ b/src/Testing/Responses/Fixtures/Threads/Runs/ThreadRunStreamResponseFixture.txt @@ -0,0 +1,41 @@ +event: thread.created +data: {"id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","object":"thread","created_at":1720104398,"metadata":{"user":"John Doe"},"tool_resources":{"code_interpreter":{"file_ids":[]}}} +event: thread.run.created +data: {"id":"run_s1X8yAjuUBlwhGrqiahzfnH7","object":"thread.run","created_at":1720104398,"assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","status":"queued","started_at ":null,"expires_at ":1720104998,"cancelled_at ":null,"failed_at ":null,"completed_at ":null,"required_action ":null,"last_error ":null,"model":"gpt-4 o","instructions":"You are a very useful assistant","tools":[{"type":"code_interpreter"},{"type":"file_search","file_search":{"max_num_results":50}},{"type":"function","function":{"name":"get_weather","description":"Determine weather in my location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The city and state e.g. San Francisco, CA"},"unit":{"type":"string","enum":["c","f"]}},"required":["location"]}}}],"tool_resources":{"code_interpreter":{"file_ids":[]}},"metadata":{"user":"John Doe"},"temperature":0.7,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true} +event: thread.run.queued +data: {"id":"run_s1X8yAjuUBlwhGrqiahzfnH7","object":"thread.run","created_at":1720104398,"assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","status":"queued","started_at":null,"expires_at":1720104998,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":"You are a very useful assistant.","tools":[{"type":"code_interpreter"},{"type":"file_search","file_search":{"max_num_results":50}},{"type":"function","function":{"name":"get_weather","description":"Determine weather in my location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The city and state e.g. San Francisco, CA"},"unit":{"type":"string","enum":["c","f"]}},"required":["location"]}}}],"tool_resources":{"code_interpreter":{"file_ids":[]}},"metadata":{"user":"John Doe"},"temperature":0.7,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true} +event: thread.run.in_progress +data: {"id":"run_s1X8yAjuUBlwhGrqiahzfnH7","object":"thread.run","created_at":1720104398,"assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","status":"in_progress","started_at":1720104398,"expires_at":1720104998,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":"You are a very useful assistant.","tools":[{"type":"code_interpreter"},{"type":"file_search","file_search":{"max_num_results":50}},{"type":"function","function":{"name":"get_weather","description":"Determine weather in my location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The city and state e.g. San Francisco, CA"},"unit":{"type":"string","enum":["c","f"]}},"required":["location"]}}}],"tool_resources":{"code_interpreter":{"file_ids":[]}},"metadata":{"user":"John Doe"},"temperature":0.7,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true} +event: thread.run.step.created +data: {"id":"step_3P1u5J5Rs95lypEfvQ3rMdPL","object":"thread.run.step","created_at":1720104399,"run_id":"run_s1X8yAjuUBlwhGrqiahzfnH7","assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","type":"message_creation","status":"in_progress","cancelled_at":null,"completed_at":null,"expires_at":1720104998,"failed_at":null,"last_error":null,"step_details":{"type":"message_creation","message_creation":{"message_id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd"}},"usage":null} +event: thread.run.step.in_progress +data: {"id":"step_3P1u5J5Rs95lypEfvQ3rMdPL","object":"thread.run.step","created_at":1720104399,"run_id":"run_s1X8yAjuUBlwhGrqiahzfnH7","assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","type":"message_creation","status":"in_progress","cancelled_at":null,"completed_at":null,"expires_at":1720104998,"failed_at":null,"last_error":null,"step_details":{"type":"message_creation","message_creation":{"message_id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd"}},"usage":null} +event: thread.message.created +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message","created_at":1720104399,"assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","run_id":"run_s1X8yAjuUBlwhGrqiahzfnH7","status":"in_progress","incomplete_details":null,"incomplete_at":null,"completed_at":null,"role":"assistant","content":[],"attachments":[],"metadata":{}} +event: thread.message.in_progress +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message","created_at":1720104399,"assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","run_id":"run_s1X8yAjuUBlwhGrqiahzfnH7","status":"in_progress","incomplete_details":null,"incomplete_at":null,"completed_at":null,"role":"assistant","content":[],"attachments":[],"metadata":{}} +event: thread.message.delta +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":"Hello","annotations":[]}}]}} +event: thread.message.delta +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":"!"}}]}} +event: thread.message.delta +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":" How"}}]}} +event: thread.message.delta +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":" can"}}]}} +event: thread.message.delta +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":" I"}}]}} +event: thread.message.delta +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":" assist"}}]}} +event: thread.message.delta +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":" you"}}]}} +event: thread.message.delta +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":" today"}}]}} +event: thread.message.delta +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":"?"}}]}} +event: thread.message.completed +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message","created_at":1720104399,"assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","run_id":"run_s1X8yAjuUBlwhGrqiahzfnH7","status":"completed","incomplete_details":null,"incomplete_at":null,"completed_at":1720104399,"role":"assistant","content":[{"type":"text","text":{"value":"Hello! How can I assist you today?","annotations":[]}}],"attachments":[],"metadata":{}} +event: thread.run.step.completed +data: {"id":"step_3P1u5J5Rs95lypEfvQ3rMdPL","object":"thread.run.step","created_at":1720104399,"run_id":"run_s1X8yAjuUBlwhGrqiahzfnH7","assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","type":"message_creation","status":"completed","cancelled_at":null,"completed_at":1720104399,"expires_at":1720104998,"failed_at":null,"last_error":null,"step_details":{"type":"message_creation","message_creation":{"message_id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd"}},"usage":{"prompt_tokens":1138,"completion_tokens":11,"total_tokens":1149}} +event: thread.run.completed +data: {"id":"run_s1X8yAjuUBlwhGrqiahzfnH7","object":"thread.run","created_at":1720104398,"assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","status":"completed","started_at":1720104398,"expires_at":null,"cancelled_at":null,"failed_at":null,"completed_at":1720104399,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":"You are a very useful assistant.","tools":[{"type":"code_interpreter"},{"type":"file_search","file_search":{"max_num_results":50}},{"type":"function","function":{"name":"get_weather","description":"Determine weather in my location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The city and state e.g. San Francisco, CA"},"unit":{"type":"string","enum":["c","f"]}},"required":["location"]}}}],"tool_resources":{"code_interpreter":{"file_ids":[]}},"metadata":{"user":"John Doe"},"temperature":0.7,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":{"prompt_tokens":1138,"completion_tokens":11,"total_tokens":1149},"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true} +event: done diff --git a/tests/Fixtures/Streams/ThreadRunStreamInvalidEventResponse.txt b/tests/Fixtures/Streams/ThreadRunStreamInvalidEventResponse.txt new file mode 100644 index 00000000..cc97d535 --- /dev/null +++ b/tests/Fixtures/Streams/ThreadRunStreamInvalidEventResponse.txt @@ -0,0 +1,2 @@ +event: this.is.invalid +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message","created_at":1720104399,"assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","run_id":"run_s1X8yAjuUBlwhGrqiahzfnH7","status":"completed","incomplete_details":null,"incomplete_at":null,"completed_at":1720104399,"role":"assistant","content":[{"type":"text","text":{"value":"Hello! How can I assist you today?","annotations":[]}}],"attachments":[],"metadata":{}} diff --git a/tests/Fixtures/Streams/ThreadRunStreamMessageDeltaResponse.txt b/tests/Fixtures/Streams/ThreadRunStreamMessageDeltaResponse.txt new file mode 100644 index 00000000..0603c61b --- /dev/null +++ b/tests/Fixtures/Streams/ThreadRunStreamMessageDeltaResponse.txt @@ -0,0 +1,2 @@ +event: thread.message.delta +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":"Hello","annotations":[]}}]}} diff --git a/tests/Fixtures/Streams/ThreadRunStreamMessageResponse.txt b/tests/Fixtures/Streams/ThreadRunStreamMessageResponse.txt new file mode 100644 index 00000000..ad149f14 --- /dev/null +++ b/tests/Fixtures/Streams/ThreadRunStreamMessageResponse.txt @@ -0,0 +1,2 @@ +event: thread.message.completed +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message","created_at":1720104399,"assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","run_id":"run_s1X8yAjuUBlwhGrqiahzfnH7","status":"completed","incomplete_details":null,"incomplete_at":null,"completed_at":1720104399,"role":"assistant","content":[{"type":"text","text":{"value":"Hello! How can I assist you today?","annotations":[]}}],"attachments":[],"metadata":{}} diff --git a/tests/Fixtures/Streams/ThreadRunStreamResponse.txt b/tests/Fixtures/Streams/ThreadRunStreamResponse.txt new file mode 100644 index 00000000..1138e695 --- /dev/null +++ b/tests/Fixtures/Streams/ThreadRunStreamResponse.txt @@ -0,0 +1,41 @@ +event: thread.created +data: {"id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","object":"thread","created_at":1720104398,"metadata":{"user":"John Doe"},"tool_resources":{"code_interpreter":{"file_ids":[]}}} +event: thread.run.created +data: {"id":"run_s1X8yAjuUBlwhGrqiahzfnH7","object":"thread.run","created_at":1720104398,"assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","status":"queued","started_at":null,"expires_at":1720104998,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":null,"last_error":null,"model":"gpt-4 o","instructions":"You are a very useful assistant","tools":[{"type":"code_interpreter"},{"type":"file_search","file_search":{"max_num_results":50}},{"type":"function","function":{"name":"get_weather","description":"Determine weather in my location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The city and state e.g. San Francisco, CA"},"unit":{"type":"string","enum":["c","f"]}},"required":["location"]}}}],"tool_resources":{"code_interpreter":{"file_ids":[]}},"metadata":{"user":"John Doe"},"temperature":0.7,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true} +event: thread.run.queued +data: {"id":"run_s1X8yAjuUBlwhGrqiahzfnH7","object":"thread.run","created_at":1720104398,"assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","status":"queued","started_at":null,"expires_at":1720104998,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":"You are a very useful assistant.","tools":[{"type":"code_interpreter"},{"type":"file_search","file_search":{"max_num_results":50}},{"type":"function","function":{"name":"get_weather","description":"Determine weather in my location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The city and state e.g. San Francisco, CA"},"unit":{"type":"string","enum":["c","f"]}},"required":["location"]}}}],"tool_resources":{"code_interpreter":{"file_ids":[]}},"metadata":{"user":"John Doe"},"temperature":0.7,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true} +event: thread.run.in_progress +data: {"id":"run_s1X8yAjuUBlwhGrqiahzfnH7","object":"thread.run","created_at":1720104398,"assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","status":"in_progress","started_at":1720104398,"expires_at":1720104998,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":"You are a very useful assistant.","tools":[{"type":"code_interpreter"},{"type":"file_search","file_search":{"max_num_results":50}},{"type":"function","function":{"name":"get_weather","description":"Determine weather in my location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The city and state e.g. San Francisco, CA"},"unit":{"type":"string","enum":["c","f"]}},"required":["location"]}}}],"tool_resources":{"code_interpreter":{"file_ids":[]}},"metadata":{"user":"John Doe"},"temperature":0.7,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true} +event: thread.run.step.created +data: {"id":"step_3P1u5J5Rs95lypEfvQ3rMdPL","object":"thread.run.step","created_at":1720104399,"run_id":"run_s1X8yAjuUBlwhGrqiahzfnH7","assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","type":"message_creation","status":"in_progress","cancelled_at":null,"completed_at":null,"expires_at":1720104998,"failed_at":null,"last_error":null,"step_details":{"type":"message_creation","message_creation":{"message_id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd"}},"usage":null} +event: thread.run.step.in_progress +data: {"id":"step_3P1u5J5Rs95lypEfvQ3rMdPL","object":"thread.run.step","created_at":1720104399,"run_id":"run_s1X8yAjuUBlwhGrqiahzfnH7","assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","type":"message_creation","status":"in_progress","cancelled_at":null,"completed_at":null,"expires_at":1720104998,"failed_at":null,"last_error":null,"step_details":{"type":"message_creation","message_creation":{"message_id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd"}},"usage":null} +event: thread.message.created +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message","created_at":1720104399,"assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","run_id":"run_s1X8yAjuUBlwhGrqiahzfnH7","status":"in_progress","incomplete_details":null,"incomplete_at":null,"completed_at":null,"role":"assistant","content":[],"attachments":[],"metadata":{}} +event: thread.message.in_progress +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message","created_at":1720104399,"assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","run_id":"run_s1X8yAjuUBlwhGrqiahzfnH7","status":"in_progress","incomplete_details":null,"incomplete_at":null,"completed_at":null,"role":"assistant","content":[],"attachments":[],"metadata":{}} +event: thread.message.delta +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":"Hello","annotations":[]}}]}} +event: thread.message.delta +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":"!"}}]}} +event: thread.message.delta +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":" How"}}]}} +event: thread.message.delta +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":" can"}}]}} +event: thread.message.delta +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":" I"}}]}} +event: thread.message.delta +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":" assist"}}]}} +event: thread.message.delta +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":" you"}}]}} +event: thread.message.delta +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":" today"}}]}} +event: thread.message.delta +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message.delta","delta":{"content":[{"index":0,"type":"text","text":{"value":"?"}}]}} +event: thread.message.completed +data: {"id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd","object":"thread.message","created_at":1720104399,"assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","run_id":"run_s1X8yAjuUBlwhGrqiahzfnH7","status":"completed","incomplete_details":null,"incomplete_at":null,"completed_at":1720104399,"role":"assistant","content":[{"type":"text","text":{"value":"Hello! How can I assist you today?","annotations":[]}}],"attachments":[],"metadata":{}} +event: thread.run.step.completed +data: {"id":"step_3P1u5J5Rs95lypEfvQ3rMdPL","object":"thread.run.step","created_at":1720104399,"run_id":"run_s1X8yAjuUBlwhGrqiahzfnH7","assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","type":"message_creation","status":"completed","cancelled_at":null,"completed_at":1720104399,"expires_at":1720104998,"failed_at":null,"last_error":null,"step_details":{"type":"message_creation","message_creation":{"message_id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd"}},"usage":{"prompt_tokens":1138,"completion_tokens":11,"total_tokens":1149}} +event: thread.run.completed +data: {"id":"run_s1X8yAjuUBlwhGrqiahzfnH7","object":"thread.run","created_at":1720104398,"assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","status":"completed","started_at":1720104398,"expires_at":null,"cancelled_at":null,"failed_at":null,"completed_at":1720104399,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":"You are a very useful assistant.","tools":[{"type":"code_interpreter"},{"type":"file_search","file_search":{"max_num_results":50}},{"type":"function","function":{"name":"get_weather","description":"Determine weather in my location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The city and state e.g. San Francisco, CA"},"unit":{"type":"string","enum":["c","f"]}},"required":["location"]}}}],"tool_resources":{"code_interpreter":{"file_ids":[]}},"metadata":{"user":"John Doe"},"temperature":0.7,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":{"prompt_tokens":1138,"completion_tokens":11,"total_tokens":1149},"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true} +event: done diff --git a/tests/Fixtures/Streams/ThreadRunStreamRunStepDeltaResponse.txt b/tests/Fixtures/Streams/ThreadRunStreamRunStepDeltaResponse.txt new file mode 100644 index 00000000..7db20a2d --- /dev/null +++ b/tests/Fixtures/Streams/ThreadRunStreamRunStepDeltaResponse.txt @@ -0,0 +1,2 @@ +event: thread.run.step.delta +data: {"id":"step_rQmJPtF2uOyGhSCCHqk1zoVd","object":"thread.run.step.delta","delta":{"step_details":{"type":"tool_calls","tool_calls":[{"index":0,"type":"code_interpreter","code_interpreter":{"input":" '"}}]}}} diff --git a/tests/Fixtures/Streams/ThreadRunStreamStepRunCreatedResponse.txt b/tests/Fixtures/Streams/ThreadRunStreamStepRunCreatedResponse.txt new file mode 100644 index 00000000..6c56e68e --- /dev/null +++ b/tests/Fixtures/Streams/ThreadRunStreamStepRunCreatedResponse.txt @@ -0,0 +1,2 @@ +event: thread.run.step.created +data: {"id":"step_3P1u5J5Rs95lypEfvQ3rMdPL","object":"thread.run.step","created_at":1720104399,"run_id":"run_s1X8yAjuUBlwhGrqiahzfnH7","assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","type":"message_creation","status":"in_progress","cancelled_at":null,"completed_at":null,"expires_at":1720104998,"failed_at":null,"last_error":null,"step_details":{"type":"message_creation","message_creation":{"message_id":"msg_zKgPBqNcqb7qYP2bBA3tVyTd"}},"usage":null} diff --git a/tests/Fixtures/Streams/ThreadRunStreamThreadCreatedResponse.txt b/tests/Fixtures/Streams/ThreadRunStreamThreadCreatedResponse.txt new file mode 100644 index 00000000..f7423ab8 --- /dev/null +++ b/tests/Fixtures/Streams/ThreadRunStreamThreadCreatedResponse.txt @@ -0,0 +1,2 @@ +event: thread.run.created +data: {"id":"run_s1X8yAjuUBlwhGrqiahzfnH7","object":"thread.run","created_at":1720104398,"assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","status":"queued","started_at":null,"expires_at":1720104998,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":null,"last_error":null,"model":"gpt-4 o","instructions":"You are a very useful assistant","tools":[{"type":"code_interpreter"},{"type":"file_search","file_search":{"max_num_results":50}},{"type":"function","function":{"name":"get_weather","description":"Determine weather in my location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The city and state e.g. San Francisco, CA"},"unit":{"type":"string","enum":["c","f"]}},"required":["location"]}}}],"tool_resources":{"code_interpreter":{"file_ids":[]}},"metadata":{"user":"John Doe"},"temperature":0.7,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true} diff --git a/tests/Fixtures/ThreadRun.php b/tests/Fixtures/ThreadRun.php index 8559cf45..2f75ae4b 100644 --- a/tests/Fixtures/ThreadRun.php +++ b/tests/Fixtures/ThreadRun.php @@ -355,3 +355,51 @@ function threadRunListResource(): array 'has_more' => false, ]; } + +/** + * @return resource + */ +function messageDeltaThreadRunStream() +{ + return fopen(__DIR__.'/Streams/ThreadRunStreamMessageDeltaResponse.txt', 'r'); +} + +/** + * @return resource + */ +function runCreatedThreadRunStream() +{ + return fopen(__DIR__.'/Streams/ThreadRunStreamThreadCreatedResponse.txt', 'r'); +} + +/** + * @return resource + */ +function runCreatedThreadStepCreatedStream() +{ + return fopen(__DIR__.'/Streams/ThreadRunStreamStepRunCreatedResponse.txt', 'r'); +} + +/** + * @return resource + */ +function runCreatedThreadMessageCreatedStream() +{ + return fopen(__DIR__.'/Streams/ThreadRunStreamMessageResponse.txt', 'r'); +} + +/** + * @return resource + */ +function runCreatedThreadRunStepDeltaStream() +{ + return fopen(__DIR__.'/Streams/ThreadRunStreamRunStepDeltaResponse.txt', 'r'); +} + +/** + * @return resource + */ +function runCreatedThreadInvalidEventStream() +{ + return fopen(__DIR__.'/Streams/ThreadRunStreamInvalidEventResponse.txt', 'r'); +} diff --git a/tests/Responses/Threads/Runs/ThreadRunStreamResponse.php b/tests/Responses/Threads/Runs/ThreadRunStreamResponse.php new file mode 100644 index 00000000..23c4a25a --- /dev/null +++ b/tests/Responses/Threads/Runs/ThreadRunStreamResponse.php @@ -0,0 +1,65 @@ +getIterator()->current()->response) + ->toBeInstanceOf(ThreadResponse::class) + ->id->toBe('thread_sSbvUX4J1FqlUZBv6BaBbOj4'); +}); + +test('handles message delta', function () { + $response = ThreadRunStreamResponse::fake(messageDeltaThreadRunStream()); + + expect($response->getIterator()->current()->response) + ->toBeInstanceOf(ThreadMessageDeltaResponse::class) + ->id->toBe('msg_zKgPBqNcqb7qYP2bBA3tVyTd'); +}); + +test('handles thread created', function () { + $response = ThreadRunStreamResponse::fake(runCreatedThreadRunStream()); + + expect($response->getIterator()->current()->response) + ->toBeInstanceOf(ThreadRunResponse::class) + ->id->toBe('run_s1X8yAjuUBlwhGrqiahzfnH7'); +}); + +test('handles thread run step', function () { + $response = ThreadRunStreamResponse::fake(runCreatedThreadStepCreatedStream()); + + expect($response->getIterator()->current()->response) + ->toBeInstanceOf(ThreadRunStepResponse::class) + ->id->toBe('step_3P1u5J5Rs95lypEfvQ3rMdPL'); +}); + +test('handles message created', function () { + $response = ThreadRunStreamResponse::fake(runCreatedThreadMessageCreatedStream()); + + expect($response->getIterator()->current()->response) + ->toBeInstanceOf(ThreadMessageResponse::class) + ->id->toBe('msg_zKgPBqNcqb7qYP2bBA3tVyTd'); +}); + +test('handles thread run delta', function () { + $response = ThreadRunStreamResponse::fake(runCreatedThreadRunStepDeltaStream()); + + expect($response->getIterator()->current()->response) + ->toBeInstanceOf(ThreadRunStepDeltaResponse::class) + ->id->toBe('step_rQmJPtF2uOyGhSCCHqk1zoVd'); +}); + +test('handles invalid event', function () { + $response = ThreadRunStreamResponse::fake(runCreatedThreadInvalidEventStream()); + + expect(fn () => $response->getIterator()->current()->response) + ->toThrow(UnknownEventException::class); +}); From 067ab09561577775e385c3a81f203e031a616e7d Mon Sep 17 00:00:00 2001 From: Alberto Peripolli Date: Fri, 11 Apr 2025 12:43:02 +0200 Subject: [PATCH 16/29] docs: update gpt-4 to gpt-4o on readme (#427) --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 43894024..f8780a01 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ $yourApiKey = getenv('YOUR_API_KEY'); $client = OpenAI::client($yourApiKey); $result = $client->chat()->create([ - 'model' => 'gpt-4', + 'model' => 'gpt-4o', 'messages' => [ ['role' => 'user', 'content' => 'Hello!'], ], @@ -297,7 +297,7 @@ Creates a streamed completion for the chat message. ```php $stream = $client->chat()->createStreamed([ - 'model' => 'gpt-4', + 'model' => 'gpt-4o', 'messages' => [ ['role' => 'user', 'content' => 'Hello!'], ], @@ -846,7 +846,7 @@ $response = $client->assistants()->create([ 'type' => 'code_interpreter', ], ], - 'model' => 'gpt-4', + 'model' => 'gpt-4o', ]); $response->id; // 'asst_gxzBkD1wkKEloYqZ410pT5pd' @@ -854,7 +854,7 @@ $response->object; // 'assistant' $response->createdAt; // 1623936000 $response->name; // 'Math Tutor' $response->instructions; // 'You are a personal math tutor. When asked a question, write and run Python code to answer the question.' -$response->model; // 'gpt-4' +$response->model; // 'gpt-4o' $response->description; // null $response->tools[0]->type; // 'code_interpreter' $response->toolResources; // [] @@ -878,7 +878,7 @@ $response->object; // 'assistant' $response->createdAt; // 1623936000 $response->name; // 'Math Tutor' $response->instructions; // 'You are a personal math tutor. When asked a question, write and run Python code to answer the question.' -$response->model; // 'gpt-4' +$response->model; // 'gpt-4o' $response->description; // null $response->tools[0]->type; // 'code_interpreter' $response->toolResources; // [] @@ -904,7 +904,7 @@ $response->object; // 'assistant' $response->createdAt; // 1623936000 $response->name; // 'New Math Tutor' $response->instructions; // 'You are a personal math tutor. When asked a question, write and run Python code to answer the question.' -$response->model; // 'gpt-4' +$response->model; // 'gpt-4o' $response->description; // null $response->tools[0]->type; // 'code_interpreter' $response->toolResources; // [] @@ -1005,7 +1005,7 @@ $response->failedAt; // null $response->completedAt; // null $response->incompleteDetails; // null $response->lastError; // null -$response->model; // 'gpt-4' +$response->model; // 'gpt-4o' $response->instructions; // null $response->tools; // [] $response->metadata; // [] @@ -1234,7 +1234,7 @@ $response->failedAt; // null $response->completedAt; // null $response->incompleteDetails; // null $response->lastError; // null -$response->model; // 'gpt-4' +$response->model; // 'gpt-4o' $response->instructions; // null $response->tools; // [] $response->metadata; // [] @@ -1349,7 +1349,7 @@ $response->failedAt; // null $response->completedAt; // null $response->incompleteDetails; // null $response->lastError; // null -$response->model; // 'gpt-4' +$response->model; // 'gpt-4o' $response->instructions; // null $response->tools; // [] $response->metadata; // [] @@ -1395,7 +1395,7 @@ $response->failedAt; // null $response->completedAt; // null $response->incompleteDetails; // null $response->lastError; // null -$response->model; // 'gpt-4' +$response->model; // 'gpt-4o' $response->instructions; // null $response->tools; // [] $response->usage->total_tokens; // 579 @@ -1434,7 +1434,7 @@ $response->failedAt; // null $response->completedAt; // null $response->incompleteDetails; // null $response->lastError; // null -$response->model; // 'gpt-4' +$response->model; // 'gpt-4o' $response->instructions; // null $response->tools; // [] $response->usage?->total_tokens; // 579 @@ -1481,7 +1481,7 @@ $response->failedAt; // null $response->completedAt; // null $response->incompleteDetails; // null $response->lastError; // null -$response->model; // 'gpt-4' +$response->model; // 'gpt-4o' $response->instructions; // null $response->usage->total_tokens; // 579 $response->temperature; // null From a1fe444038cbbd96db1808949383f694ddf83d8c Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Sat, 12 Apr 2025 18:06:54 -0400 Subject: [PATCH 17/29] fix: Support streaming of non-OpenAI models that return "ping" (#556) * fix: support type: ping in streaming * test: confirm type:ping works in streaming --- src/Responses/StreamResponse.php | 6 ++++- tests/Fixtures/Chat.php | 8 +++++++ tests/Fixtures/Streams/ChatCompletionPing.txt | 9 ++++++++ tests/Resources/Chat.php | 23 +++++++++++++++++++ .../Responses/Chat/CreateStreamedResponse.php | 7 ++++++ 5 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 tests/Fixtures/Streams/ChatCompletionPing.txt diff --git a/src/Responses/StreamResponse.php b/src/Responses/StreamResponse.php index b890756f..9e2da7c2 100644 --- a/src/Responses/StreamResponse.php +++ b/src/Responses/StreamResponse.php @@ -53,13 +53,17 @@ public function getIterator(): Generator break; } - /** @var array{error?: array{message: string|array, type: string, code: string}} $response */ + /** @var array{error?: array{message: string|array, type: string, code: string}, type?: string} $response */ $response = json_decode($data, true, flags: JSON_THROW_ON_ERROR); if (isset($response['error'])) { throw new ErrorException($response['error'], $this->response->getStatusCode()); } + if (isset($response['type']) && $response['type'] === 'ping') { + continue; + } + if ($event !== null) { $response['__event'] = $event; $response['__meta'] = $this->meta(); diff --git a/tests/Fixtures/Chat.php b/tests/Fixtures/Chat.php index aaae5894..fd91e41d 100644 --- a/tests/Fixtures/Chat.php +++ b/tests/Fixtures/Chat.php @@ -466,6 +466,14 @@ function chatCompletionStream() return fopen(__DIR__.'/Streams/ChatCompletionCreate.txt', 'r'); } +/** + * @return resource + */ +function chatCompletionStreamPing() +{ + return fopen(__DIR__.'/Streams/ChatCompletionPing.txt', 'r'); +} + /** * @return resource */ diff --git a/tests/Fixtures/Streams/ChatCompletionPing.txt b/tests/Fixtures/Streams/ChatCompletionPing.txt new file mode 100644 index 00000000..a6abb22a --- /dev/null +++ b/tests/Fixtures/Streams/ChatCompletionPing.txt @@ -0,0 +1,9 @@ +data: {"id":"msg_0111RgCFCqN68mJbev6Rq1cz","choices":[{"index":0,"delta":{"role":"assistant"}}],"created":1744469024,"model":"claude-3-7-sonnet-20250219","object":"chat.completion.chunk"} +data: {"type": "ping"} +data: {"id":"msg_0111RgCFCqN68mJbev6Rq1cz","choices":[{"index":0,"delta":{"content":"Hello!"}}],"created":1744469024,"model":"claude-3-7-sonnet-20250219","object":"chat.completion.chunk"} +data: {"id":"msg_0111RgCFCqN68mJbev6Rq1cz","choices":[{"index":0,"delta":{"content":" How can I assist you today? I'm here to help"}}],"created":1744469024,"model":"claude-3-7-sonnet-20250219","object":"chat.completion.chunk"} +data: {"id":"msg_0111RgCFCqN68mJbev6Rq1cz","choices":[{"index":0,"delta":{"content":" with information, answer questions, or discuss"}}],"created":1744469024,"model":"claude-3-7-sonnet-20250219","object":"chat.completion.chunk"} +data: {"id":"msg_0111RgCFCqN68mJbev6Rq1cz","choices":[{"index":0,"delta":{"content":" various topics. Feel free to let me know what you're"}}],"created":1744469024,"model":"claude-3-7-sonnet-20250219","object":"chat.completion.chunk"} +data: {"id":"msg_0111RgCFCqN68mJbev6Rq1cz","choices":[{"index":0,"delta":{"content":" interested in talking about."}}],"created":1744469024,"model":"claude-3-7-sonnet-20250219","object":"chat.completion.chunk"} +data: {"id":"msg_0111RgCFCqN68mJbev6Rq1cz","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"created":1744469024,"model":"claude-3-7-sonnet-20250219","object":"chat.completion.chunk"} +data: [DONE] diff --git a/tests/Resources/Chat.php b/tests/Resources/Chat.php index 4d639b19..107a77c3 100644 --- a/tests/Resources/Chat.php +++ b/tests/Resources/Chat.php @@ -100,6 +100,29 @@ ->toBeInstanceOf(MetaInformation::class); }); +test('handles ping messages in stream', function () { + $response = new Response( + body: new Stream(chatCompletionStreamPing()), + headers: metaHeaders(), + ); + + $client = mockStreamClient('POST', 'chat/completions', [ + 'model' => 'gpt-3.5-turbo', + 'messages' => ['role' => 'user', 'content' => 'Hello!'], + 'stream' => true, + ], $response); + + $stream = $client->chat()->createStreamed([ + 'model' => 'gpt-3.5-turbo', + 'messages' => ['role' => 'user', 'content' => 'Hello!'], + ]); + + foreach ($stream as $response) { + expect($response) + ->toBeInstanceOf(CreateStreamedResponse::class); + } +}); + test('handles error messages in stream', function () { $response = new Response( body: new Stream(chatCompletionStreamError()) diff --git a/tests/Responses/Chat/CreateStreamedResponse.php b/tests/Responses/Chat/CreateStreamedResponse.php index 80a09e54..3bc0edd0 100644 --- a/tests/Responses/Chat/CreateStreamedResponse.php +++ b/tests/Responses/Chat/CreateStreamedResponse.php @@ -73,3 +73,10 @@ expect($response->getIterator()->current()) ->id->toBe('chatcmpl-6wdIE4DsUtqf1srdMTsfkJp0VWZgz'); }); + +test('fake with ping', function () { + $response = CreateStreamedResponse::fake(chatCompletionStreamPing()); + + expect($response->getIterator()->current()) + ->id->toBe('msg_0111RgCFCqN68mJbev6Rq1cz'); +}); From bb305b03857f460151244092ade6b3c440b28bb2 Mon Sep 17 00:00:00 2001 From: Thorb <5137195+ThorbJ@users.noreply.github.com> Date: Sun, 13 Apr 2025 18:26:19 +0400 Subject: [PATCH 18/29] fix: Add compatibility for Aliyun compatibility layer for Files (#530) * Add compatibility for aliyun LLM APIs * test: add test to confirm status_details missing --------- Co-authored-by: Thorb Jiang Co-authored-by: Connor Tumbleson --- src/Responses/Files/CreateResponse.php | 2 +- src/Responses/Files/RetrieveResponse.php | 2 +- src/Responses/FineTunes/RetrieveResponseFile.php | 2 +- tests/Responses/Files/RetrieveResponse.php | 11 +++++++++++ 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Responses/Files/CreateResponse.php b/src/Responses/Files/CreateResponse.php index bba4cf46..e01197c1 100644 --- a/src/Responses/Files/CreateResponse.php +++ b/src/Responses/Files/CreateResponse.php @@ -54,7 +54,7 @@ public static function from(array $attributes, MetaInformation $meta): self $attributes['filename'], $attributes['purpose'], $attributes['status'], - $attributes['status_details'], + $attributes['status_details'] ?? null, $meta, ); } diff --git a/src/Responses/Files/RetrieveResponse.php b/src/Responses/Files/RetrieveResponse.php index 3564b4c8..d3cb2450 100644 --- a/src/Responses/Files/RetrieveResponse.php +++ b/src/Responses/Files/RetrieveResponse.php @@ -54,7 +54,7 @@ public static function from(array $attributes, MetaInformation $meta): self $attributes['filename'], $attributes['purpose'], $attributes['status'], - $attributes['status_details'], + $attributes['status_details'] ?? null, $meta, ); } diff --git a/src/Responses/FineTunes/RetrieveResponseFile.php b/src/Responses/FineTunes/RetrieveResponseFile.php index b76a1e60..01aa3fd5 100644 --- a/src/Responses/FineTunes/RetrieveResponseFile.php +++ b/src/Responses/FineTunes/RetrieveResponseFile.php @@ -46,7 +46,7 @@ public static function from(array $attributes): self $attributes['filename'], $attributes['purpose'], $attributes['status'], - $attributes['status_details'], + $attributes['status_details'] ?? null, ); } diff --git a/tests/Responses/Files/RetrieveResponse.php b/tests/Responses/Files/RetrieveResponse.php index e678e9f1..89002e18 100644 --- a/tests/Responses/Files/RetrieveResponse.php +++ b/tests/Responses/Files/RetrieveResponse.php @@ -50,6 +50,17 @@ ->meta()->toBeInstanceOf(MetaInformation::class); }); +test('from with status_details missing', function () { + $data = fileResource(); + unset($data['status_details']); + + $result = RetrieveResponse::from($data, meta()); + + expect($result) + ->toBeInstanceOf(RetrieveResponse::class) + ->statusDetails->toBeNull(); +}); + test('as array accessible', function () { $result = RetrieveResponse::from(fileResource(), meta()); From fe10efb23812d96c131c2594808475e460d2f2ed Mon Sep 17 00:00:00 2001 From: Christopher Pitt Date: Mon, 14 Apr 2025 15:06:35 +0200 Subject: [PATCH 19/29] Allow arguments to be passed to files list request (#557) * Allow arguments to be passed to files list request * chore: add files.list parameter support to contract/tests --------- Co-authored-by: Connor Tumbleson --- src/Contracts/Resources/FilesContract.php | 4 +++- src/Resources/Files.php | 4 ++-- src/Testing/Resources/FilesTestResource.php | 4 ++-- tests/Testing/Resources/FilesTestResource.php | 15 +++++++++++++++ 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/Contracts/Resources/FilesContract.php b/src/Contracts/Resources/FilesContract.php index cf748a77..3aedb8e9 100644 --- a/src/Contracts/Resources/FilesContract.php +++ b/src/Contracts/Resources/FilesContract.php @@ -13,8 +13,10 @@ interface FilesContract * Returns a list of files that belong to the user's organization. * * @see https://platform.openai.com/docs/api-reference/files/list + * + * @param array $parameters */ - public function list(): ListResponse; + public function list(array $parameters = []): ListResponse; /** * Returns information about a specific file. diff --git a/src/Resources/Files.php b/src/Resources/Files.php index 6f5490e1..1c25cdfd 100644 --- a/src/Resources/Files.php +++ b/src/Resources/Files.php @@ -21,9 +21,9 @@ final class Files implements FilesContract * * @see https://platform.openai.com/docs/api-reference/files/list */ - public function list(): ListResponse + public function list(array $parameters = []): ListResponse { - $payload = Payload::list('files'); + $payload = Payload::list('files', $parameters); /** @var Response|string|null}>}> $response */ $response = $this->transporter->requestObject($payload); diff --git a/src/Testing/Resources/FilesTestResource.php b/src/Testing/Resources/FilesTestResource.php index cd911392..1bcd1b4a 100644 --- a/src/Testing/Resources/FilesTestResource.php +++ b/src/Testing/Resources/FilesTestResource.php @@ -19,9 +19,9 @@ protected function resource(): string return Files::class; } - public function list(): ListResponse + public function list(array $parameters = []): ListResponse { - return $this->record(__FUNCTION__); + return $this->record(__FUNCTION__, func_get_args()); } public function retrieve(string $file): RetrieveResponse diff --git a/tests/Testing/Resources/FilesTestResource.php b/tests/Testing/Resources/FilesTestResource.php index 7259fa01..1590fec2 100644 --- a/tests/Testing/Resources/FilesTestResource.php +++ b/tests/Testing/Resources/FilesTestResource.php @@ -32,6 +32,21 @@ }); }); +it('records a file list request with parameters', function () { + $fake = new ClientFake([ + ListResponse::fake(), + ]); + + $fake->files()->list([ + 'limit' => 1, + ]); + + $fake->assertSent(Files::class, function ($method, $parameters) { + return $method === 'list' && + $parameters['limit'] === 1; + }); +}); + it('records a files download request', function () { $fake = new ClientFake([ 'fake-file-content', From 6f5861f3a2fa4e7eefdbc7e4014052734957e796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luka=20Lo=C4=8Dni=C5=A1kar?= Date: Tue, 15 Apr 2025 12:32:19 +0200 Subject: [PATCH 20/29] feat: search vector store (#559) * feat(vector-stores): implement search method * chore(vector-stores): Add search documentation * test(vectore-stores): search resource and response * chore: remove extra newlines --- README.md | 34 ++++++++ .../Resources/VectorStoresContract.php | 10 +++ src/Resources/VectorStores.php | 18 +++++ .../Search/VectorStoreSearchResponse.php | 80 +++++++++++++++++++ .../VectorStoreSearchResponseContent.php | 51 ++++++++++++ .../Search/VectorStoreSearchResponseFile.php | 72 +++++++++++++++++ .../Resources/VectorStoresTestResource.php | 9 +++ ...ectorStoreSearchResponseContentFixture.php | 11 +++ .../VectorStoreSearchResponseFileFixture.php | 19 +++++ .../VectorStoreSearchResponseFixture.php | 31 +++++++ tests/Fixtures/VectorStoreSearch.php | 46 +++++++++++ .../Search/VectorStoreSearchResponse.php | 55 +++++++++++++ 12 files changed, 436 insertions(+) create mode 100644 src/Responses/VectorStores/Search/VectorStoreSearchResponse.php create mode 100644 src/Responses/VectorStores/Search/VectorStoreSearchResponseContent.php create mode 100644 src/Responses/VectorStores/Search/VectorStoreSearchResponseFile.php create mode 100644 src/Testing/Responses/Fixtures/VectorStores/Search/VectorStoreSearchResponseContentFixture.php create mode 100644 src/Testing/Responses/Fixtures/VectorStores/Search/VectorStoreSearchResponseFileFixture.php create mode 100644 src/Testing/Responses/Fixtures/VectorStores/Search/VectorStoreSearchResponseFixture.php create mode 100644 tests/Fixtures/VectorStoreSearch.php create mode 100644 tests/Responses/VectorStores/Search/VectorStoreSearchResponse.php diff --git a/README.md b/README.md index f8780a01..e59185f4 100644 --- a/README.md +++ b/README.md @@ -1929,6 +1929,40 @@ foreach ($response->data as $result) { $response->toArray(); // ['object' => 'list', ...]] ``` +#### `search` + +Search a vector store for relevant chunks based on a query and file attributes filter. + +```php +$response = $client->vectorStores()->search( + vectorStoreId: 'vs_vzfQhlTWVUl38QGqQAoQjeDF', + parameters: [ + 'query' => 'What is the return policy?', + 'max_num_results' => 5, + 'filters' => [], + 'rewrite_query' => false + ] +); + +$response->object; // 'vector_store.search_results.page' +$response->searchQuery; // 'What is the return policy?' +$response->hasMore; // false +$response->nextPage; // null +foreach ($response->data as $file) { + $result->fileId; // 'file-fUU0hFRuQ1GzhOweTNeJlCXG' + $result->filename; // 'return_policy.pdf' + $result->score; // 0.95 + $result->attributes; // ['category' => 'customer_service'] + + foreach ($result->content as $content) { + $content->type; // 'text' + $content->text; // 'Our return policy allows customers to return items within 30 days...' + } +} + +$response->toArray(); // ['object' => 'vector_store.search_results.page', ...] +``` + ### `Vector Store File Batches` Resource #### `create` diff --git a/src/Contracts/Resources/VectorStoresContract.php b/src/Contracts/Resources/VectorStoresContract.php index 2d2010ed..4f5c1120 100644 --- a/src/Contracts/Resources/VectorStoresContract.php +++ b/src/Contracts/Resources/VectorStoresContract.php @@ -2,6 +2,7 @@ namespace OpenAI\Contracts\Resources; +use OpenAI\Responses\VectorStores\Search\VectorStoreSearchResponse; use OpenAI\Responses\VectorStores\VectorStoreDeleteResponse; use OpenAI\Responses\VectorStores\VectorStoreListResponse; use OpenAI\Responses\VectorStores\VectorStoreResponse; @@ -62,4 +63,13 @@ public function files(): VectorStoresFilesContract; * @see https://platform.openai.com/docs/api-reference/vector-stores-file-batches */ public function batches(): VectorStoresFileBatchesContract; + + /** + * Search a vector store for relevant chunks based on a query and file attributes filter. + * + * @see https://platform.openai.com/docs/api-reference/vector-stores/search + * + * @param array $parameters + */ + public function search(string $vectorStoreId, array $parameters = []): VectorStoreSearchResponse; } diff --git a/src/Resources/VectorStores.php b/src/Resources/VectorStores.php index bb951b74..5918b0a7 100644 --- a/src/Resources/VectorStores.php +++ b/src/Resources/VectorStores.php @@ -7,6 +7,7 @@ use OpenAI\Contracts\Resources\VectorStoresContract; use OpenAI\Contracts\Resources\VectorStoresFileBatchesContract; use OpenAI\Contracts\Resources\VectorStoresFilesContract; +use OpenAI\Responses\VectorStores\Search\VectorStoreSearchResponse; use OpenAI\Responses\VectorStores\VectorStoreDeleteResponse; use OpenAI\Responses\VectorStores\VectorStoreListResponse; use OpenAI\Responses\VectorStores\VectorStoreResponse; @@ -117,4 +118,21 @@ public function batches(): VectorStoresFileBatchesContract { return new VectorStoresFileBatches($this->transporter); } + + /** + * Search a vector store for relevant chunks based on a query and file attributes filter. + * + * @see https://platform.openai.com/docs/api-reference/vector-stores/search + * + * @param array $parameters + */ + public function search(string $vectorStoreId, array $parameters = []): VectorStoreSearchResponse + { + $payload = Payload::create("vector_stores/{$vectorStoreId}/search", $parameters); + + /** @var Response, data: array, content: array}>, has_more: bool, next_page: ?string}> $response */ + $response = $this->transporter->requestObject($payload); + + return VectorStoreSearchResponse::from($response->data(), $response->meta()); + } } diff --git a/src/Responses/VectorStores/Search/VectorStoreSearchResponse.php b/src/Responses/VectorStores/Search/VectorStoreSearchResponse.php new file mode 100644 index 00000000..b82b9d0b --- /dev/null +++ b/src/Responses/VectorStores/Search/VectorStoreSearchResponse.php @@ -0,0 +1,80 @@ +, data: array, content: array}>, has_more: bool, next_page: ?string}> + */ +final class VectorStoreSearchResponse implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible, data: array, content: array}>, has_more: bool, next_page: ?string}> + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + /** + * @param array $data + * @param string|array $searchQuery + */ + private function __construct( + public readonly string $object, + public readonly string|array $searchQuery, + public readonly array $data, + public readonly bool $hasMore, + public readonly ?string $nextPage, + private readonly MetaInformation $meta, + ) {} + + /** + * Acts as static factory, and returns a new Response instance. + * + * @param array{object: string, search_query: string|array, data: array, content: array}>, has_more: bool, next_page: ?string} $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + $data = array_map( + static fn (array $result): VectorStoreSearchResponseFile => VectorStoreSearchResponseFile::from($result), + $attributes['data'] + ); + + return new self( + $attributes['object'], + $attributes['search_query'], + $data, + $attributes['has_more'], + $attributes['next_page'], + $meta, + ); + } + + /** + * {@inheritDoc} + * + * @return array{object: string, search_query: string|array, data: array, content: array}>, has_more: bool, next_page: ?string} + */ + public function toArray(): array + { + return [ + 'object' => $this->object, + 'search_query' => $this->searchQuery, + 'data' => array_map( + static fn (VectorStoreSearchResponseFile $item): array => $item->toArray(), + $this->data + ), + 'has_more' => $this->hasMore, + 'next_page' => $this->nextPage, + ]; + } +} diff --git a/src/Responses/VectorStores/Search/VectorStoreSearchResponseContent.php b/src/Responses/VectorStores/Search/VectorStoreSearchResponseContent.php new file mode 100644 index 00000000..5f706ca1 --- /dev/null +++ b/src/Responses/VectorStores/Search/VectorStoreSearchResponseContent.php @@ -0,0 +1,51 @@ + + */ +final class VectorStoreSearchResponseContent implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly string $type, + public readonly string $text, + ) {} + + /** + * Acts as static factory, and returns a new Response instance. + * + * @param array{type: string, text: string} $attributes + */ + public static function from(array $attributes): self + { + return new self( + $attributes['type'], + $attributes['text'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'text' => $this->text, + ]; + } +} diff --git a/src/Responses/VectorStores/Search/VectorStoreSearchResponseFile.php b/src/Responses/VectorStores/Search/VectorStoreSearchResponseFile.php new file mode 100644 index 00000000..d1dee2be --- /dev/null +++ b/src/Responses/VectorStores/Search/VectorStoreSearchResponseFile.php @@ -0,0 +1,72 @@ +, content: array}> + */ +final class VectorStoreSearchResponseFile implements ResponseContract +{ + /** + * @use ArrayAccessible, content: array}> + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $attributes + * @param array $content + */ + private function __construct( + public readonly string $fileId, + public readonly string $filename, + public readonly float $score, + public readonly array $attributes, + public readonly array $content, + ) {} + + /** + * Acts as static factory, and returns a new Response instance. + * + * @param array{file_id: string, filename: string, score: float, attributes: array, content: array} $attributes + */ + public static function from(array $attributes): self + { + $content = array_map( + static fn (array $content): VectorStoreSearchResponseContent => VectorStoreSearchResponseContent::from($content), + $attributes['content'], + ); + + return new self( + $attributes['file_id'], + $attributes['filename'], + $attributes['score'], + $attributes['attributes'], + $content, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'file_id' => $this->fileId, + 'filename' => $this->filename, + 'score' => $this->score, + 'attributes' => $this->attributes, + 'content' => array_map( + static fn (VectorStoreSearchResponseContent $content): array => $content->toArray(), + $this->content, + ), + ]; + } +} diff --git a/src/Testing/Resources/VectorStoresTestResource.php b/src/Testing/Resources/VectorStoresTestResource.php index 76d35939..b9d2fe93 100644 --- a/src/Testing/Resources/VectorStoresTestResource.php +++ b/src/Testing/Resources/VectorStoresTestResource.php @@ -6,6 +6,7 @@ use OpenAI\Contracts\Resources\VectorStoresFileBatchesContract; use OpenAI\Contracts\Resources\VectorStoresFilesContract; use OpenAI\Resources\VectorStores; +use OpenAI\Responses\VectorStores\Search\VectorStoreSearchResponse; use OpenAI\Responses\VectorStores\VectorStoreDeleteResponse; use OpenAI\Responses\VectorStores\VectorStoreListResponse; use OpenAI\Responses\VectorStores\VectorStoreResponse; @@ -45,6 +46,14 @@ public function list(array $parameters = []): VectorStoreListResponse return $this->record(__FUNCTION__, func_get_args()); } + /** + * @param array $parameters + */ + public function search(string $vectorStoreId, array $parameters = []): VectorStoreSearchResponse + { + return $this->record(__FUNCTION__, func_get_args()); + } + public function files(): VectorStoresFilesContract { return new VectorStoresFilesTestResource($this->fake); diff --git a/src/Testing/Responses/Fixtures/VectorStores/Search/VectorStoreSearchResponseContentFixture.php b/src/Testing/Responses/Fixtures/VectorStores/Search/VectorStoreSearchResponseContentFixture.php new file mode 100644 index 00000000..26fe9255 --- /dev/null +++ b/src/Testing/Responses/Fixtures/VectorStores/Search/VectorStoreSearchResponseContentFixture.php @@ -0,0 +1,11 @@ + 'text', + 'text' => 'Sample text content from the vector store.', + ]; +} diff --git a/src/Testing/Responses/Fixtures/VectorStores/Search/VectorStoreSearchResponseFileFixture.php b/src/Testing/Responses/Fixtures/VectorStores/Search/VectorStoreSearchResponseFileFixture.php new file mode 100644 index 00000000..987d58ce --- /dev/null +++ b/src/Testing/Responses/Fixtures/VectorStores/Search/VectorStoreSearchResponseFileFixture.php @@ -0,0 +1,19 @@ + 'file_abc123', + 'filename' => 'document.pdf', + 'score' => 0.95, + 'attributes' => [ + 'author' => 'John Doe', + 'date' => '2023-01-01', + ], + 'content' => [ + VectorStoreSearchResponseContentFixture::ATTRIBUTES, + ], + ]; +} diff --git a/src/Testing/Responses/Fixtures/VectorStores/Search/VectorStoreSearchResponseFixture.php b/src/Testing/Responses/Fixtures/VectorStores/Search/VectorStoreSearchResponseFixture.php new file mode 100644 index 00000000..8009c5d6 --- /dev/null +++ b/src/Testing/Responses/Fixtures/VectorStores/Search/VectorStoreSearchResponseFixture.php @@ -0,0 +1,31 @@ + 'vector_store.search_results.page', + 'search_query' => 'What is the return policy?', + 'data' => [ + VectorStoreSearchResponseFileFixture::ATTRIBUTES, + [ + 'file_id' => 'file_xyz789', + 'filename' => 'notes.txt', + 'score' => 0.89, + 'attributes' => [ + 'author' => 'Jane Smith', + 'date' => '2023-01-02', + ], + 'content' => [ + [ + 'type' => 'text', + 'text' => 'Sample text content from the vector store.', + ], + ], + ], + ], + 'has_more' => false, + 'next_page' => null, + ]; +} diff --git a/tests/Fixtures/VectorStoreSearch.php b/tests/Fixtures/VectorStoreSearch.php new file mode 100644 index 00000000..66eb1fad --- /dev/null +++ b/tests/Fixtures/VectorStoreSearch.php @@ -0,0 +1,46 @@ + + */ +function vectorStoreSearchResource(): array +{ + return [ + 'object' => 'vector_store.search_results.page', + 'search_query' => 'What is the return policy?', + 'data' => [ + [ + 'file_id' => 'file_abc123', + 'filename' => 'policy.pdf', + 'score' => 0.95, + 'attributes' => [ + 'author' => 'John Doe', + 'date' => '2023-01-01', + ], + 'content' => [ + [ + 'type' => 'text', + 'text' => 'Our return policy allows for returns within 30 days of purchase.', + ], + ], + ], + [ + 'file_id' => 'file_xyz789', + 'filename' => 'notes.txt', + 'score' => 0.89, + 'attributes' => [ + 'author' => 'Jane Smith', + 'date' => '2023-01-02', + ], + 'content' => [ + [ + 'type' => 'text', + 'text' => 'Sample text content from the vector store.', + ], + ], + ], + ], + 'has_more' => false, + 'next_page' => null, + ]; +} diff --git a/tests/Responses/VectorStores/Search/VectorStoreSearchResponse.php b/tests/Responses/VectorStores/Search/VectorStoreSearchResponse.php new file mode 100644 index 00000000..94c8487b --- /dev/null +++ b/tests/Responses/VectorStores/Search/VectorStoreSearchResponse.php @@ -0,0 +1,55 @@ +object->toBe('vector_store.search_results.page') + ->searchQuery->toBe('What is the return policy?') + ->data->toBeArray()->toHaveCount(2) + ->data->{0}->toBeInstanceOf(VectorStoreSearchResponseFile::class) + ->hasMore->toBe(false) + ->nextPage->toBe(null); +}); + +test('as array accessible', function () { + $result = VectorStoreSearchResponse::from(vectorStoreSearchResource(), meta()); + + expect($result['search_query']) + ->toBe('What is the return policy?'); +}); + +test('to array', function () { + $result = VectorStoreSearchResponse::from(vectorStoreSearchResource(), meta()); + + expect($result->toArray()) + ->toBe(vectorStoreSearchResource()); +}); + +test('fake', function () { + $response = VectorStoreSearchResponse::fake(); + + expect($response) + ->object->toBe('vector_store.search_results.page') + ->searchQuery->toBe('What is the return policy?') + ->hasMore->toBe(false) + ->nextPage->toBe(null); +}); + +test('fake with override', function () { + $response = VectorStoreSearchResponse::fake([ + 'object' => 'custom_object', + 'search_query' => 'Custom query', + 'has_more' => false, + 'next_page' => null, + ]); + + expect($response) + ->object->toBe('custom_object') + ->searchQuery->toBe('Custom query') + ->hasMore->toBe(false) + ->nextPage->toBeNull(); +}); From 205be4abd2c2c3856829a1a679f9140dce1fbf83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luka=20Lo=C4=8Dni=C5=A1kar?= Date: Thu, 17 Apr 2025 22:53:04 +0200 Subject: [PATCH 21/29] fix(OpenRouter): Fix token usage response (#560) * chore: add fallback values to response token details for OpenRouter compatibility * test: diffrent OpenRouter model responses --- ...teResponseUsageCompletionTokensDetails.php | 34 +++-- ...CreateResponseUsagePromptTokensDetails.php | 2 +- tests/Fixtures/Chat.php | 142 ++++++++++++++++++ tests/Responses/Chat/CreateResponse.php | 64 ++++++++ tests/Responses/Chat/CreateResponseChoice.php | 30 ++++ tests/Responses/Chat/CreateResponseUsage.php | 44 ++++++ 6 files changed, 302 insertions(+), 14 deletions(-) diff --git a/src/Responses/Chat/CreateResponseUsageCompletionTokensDetails.php b/src/Responses/Chat/CreateResponseUsageCompletionTokensDetails.php index 989ee27b..c7dc2552 100644 --- a/src/Responses/Chat/CreateResponseUsageCompletionTokensDetails.php +++ b/src/Responses/Chat/CreateResponseUsageCompletionTokensDetails.php @@ -8,34 +8,42 @@ final class CreateResponseUsageCompletionTokensDetails { private function __construct( public readonly ?int $audioTokens, - public readonly int $reasoningTokens, - public readonly int $acceptedPredictionTokens, - public readonly int $rejectedPredictionTokens + public readonly ?int $reasoningTokens, + public readonly ?int $acceptedPredictionTokens, + public readonly ?int $rejectedPredictionTokens ) {} /** - * @param array{audio_tokens?:int, reasoning_tokens:int, accepted_prediction_tokens:int, rejected_prediction_tokens:int} $attributes + * @param array{audio_tokens?:int|null, reasoning_tokens?:int|null, accepted_prediction_tokens?:int|null, rejected_prediction_tokens?:int|null} $attributes */ public static function from(array $attributes): self { return new self( $attributes['audio_tokens'] ?? null, - $attributes['reasoning_tokens'], - $attributes['accepted_prediction_tokens'], - $attributes['rejected_prediction_tokens'], + $attributes['reasoning_tokens'] ?? null, + $attributes['accepted_prediction_tokens'] ?? null, + $attributes['rejected_prediction_tokens'] ?? null, ); } /** - * @return array{audio_tokens?:int, reasoning_tokens:int, accepted_prediction_tokens:int, rejected_prediction_tokens:int} + * @return array{audio_tokens?:int, reasoning_tokens?:int, accepted_prediction_tokens?:int, rejected_prediction_tokens?:int} */ public function toArray(): array { - $result = [ - 'reasoning_tokens' => $this->reasoningTokens, - 'accepted_prediction_tokens' => $this->acceptedPredictionTokens, - 'rejected_prediction_tokens' => $this->rejectedPredictionTokens, - ]; + $result = []; + + if (! is_null($this->reasoningTokens)) { + $result['reasoning_tokens'] = $this->reasoningTokens; + } + + if (! is_null($this->acceptedPredictionTokens)) { + $result['accepted_prediction_tokens'] = $this->acceptedPredictionTokens; + } + + if (! is_null($this->rejectedPredictionTokens)) { + $result['rejected_prediction_tokens'] = $this->rejectedPredictionTokens; + } if (! is_null($this->audioTokens)) { $result['audio_tokens'] = $this->audioTokens; diff --git a/src/Responses/Chat/CreateResponseUsagePromptTokensDetails.php b/src/Responses/Chat/CreateResponseUsagePromptTokensDetails.php index 5cd0e094..fc81a41a 100644 --- a/src/Responses/Chat/CreateResponseUsagePromptTokensDetails.php +++ b/src/Responses/Chat/CreateResponseUsagePromptTokensDetails.php @@ -12,7 +12,7 @@ private function __construct( ) {} /** - * @param array{audio_tokens?:int, cached_tokens?: int} $attributes + * @param array{audio_tokens?:int|null, cached_tokens?:int} $attributes */ public static function from(array $attributes): self { diff --git a/tests/Fixtures/Chat.php b/tests/Fixtures/Chat.php index fd91e41d..a30996a5 100644 --- a/tests/Fixtures/Chat.php +++ b/tests/Fixtures/Chat.php @@ -37,6 +37,148 @@ function chatCompletion(): array ]; } +/** + * @return array + */ +function chatCompletionOpenRouter(): array +{ + return [ + 'id' => 'gen-123', + 'object' => 'chat.completion', + 'created' => 1744873707, + 'model' => 'mistral/ministral-8b', + 'choices' => [ + [ + 'index' => 0, + 'message' => [ + 'role' => 'assistant', + 'content' => 'Hello! How can I assist you today?', + ], + 'logprobs' => null, + 'finish_reason' => 'stop', + ], + ], + 'usage' => [ + 'prompt_tokens' => 13, + 'completion_tokens' => 20, + 'total_tokens' => 33, + ], + ]; +} + +/** + * @return array + */ +function chatCompletionOpenRouterOpenAI(): array +{ + return [ + 'id' => 'gen-123', + 'provider' => 'OpenAI', + 'model' => 'openai/gpt-4o-mini', + 'object' => 'chat.completion', + 'created' => 1744900650, + 'system_fingerprint' => 'fp_0392822090', + 'choices' => [ + [ + 'index' => 0, + 'message' => [ + 'role' => 'assistant', + 'content' => 'Hello! How can I assist you today?', + 'refusal' => null, + 'reasoning' => null, + ], + 'logprobs' => null, + 'finish_reason' => 'stop', + 'native_finish_reason' => 'stop', + ], + ], + 'usage' => [ + 'prompt_tokens' => 21, + 'completion_tokens' => 21, + 'total_tokens' => 42, + 'prompt_tokens_details' => [ + 'cached_tokens' => 0, + ], + 'completion_tokens_details' => [ + 'reasoning_tokens' => 0, + ], + ], + ]; +} + +/** + * @return array + */ +function chatCompletionOpenRouterGoogle(): array +{ + return [ + 'id' => 'gen-123', + 'provider' => 'Google', + 'model' => 'google/gemini-2.5-pro-preview-03-25', + 'object' => 'chat.completion', + 'created' => 1744910839, + 'choices' => [ + [ + 'index' => 0, + 'message' => [ + 'role' => 'assistant', + 'content' => 'Hello there! I\'m a large language model, trained by Google.', + 'refusal' => null, + 'reasoning' => null, + ], + 'logprobs' => null, + 'finish_reason' => 'stop', + 'native_finish_reason' => 'STOP', + ], + ], + 'usage' => [ + 'prompt_tokens' => 10, + 'completion_tokens' => 138, + 'total_tokens' => 148, + ], + ]; +} + +/** + * @return array + */ +function chatCompletionOpenRouterXAI(): array +{ + return [ + 'id' => 'gen-123', + 'provider' => 'xAI', + 'model' => 'x-ai/grok-3-mini-beta', + 'object' => 'chat.completion', + 'created' => 1744911228, + 'system_fingerprint' => 'fp_d133ae3397', + 'choices' => [ + [ + 'index' => 0, + 'message' => [ + 'role' => 'assistant', + 'content' => 'Hello! I\'m Grok, an AI model created by xAI.', + 'refusal' => null, + 'reasoning' => 'First, the user is asking "Hello! what model are you?"', + ], + 'logprobs' => null, + 'finish_reason' => 'stop', + 'native_finish_reason' => 'stop', + ], + ], + 'usage' => [ + 'prompt_tokens' => 21, + 'completion_tokens' => 36, + 'total_tokens' => 392, + 'prompt_tokens_details' => [ + 'cached_tokens' => 0, + ], + 'completion_tokens_details' => [ + 'reasoning_tokens' => 335, + ], + ], + ]; +} + /** * @return array */ diff --git a/tests/Responses/Chat/CreateResponse.php b/tests/Responses/Chat/CreateResponse.php index caafedf5..2538d26c 100644 --- a/tests/Responses/Chat/CreateResponse.php +++ b/tests/Responses/Chat/CreateResponse.php @@ -203,3 +203,67 @@ ->function->name->toBe('get_current_weather') ->function->arguments->toBe("{\n \"location\": \"Boston, MA\"\n}"); }); + +test('from (OpenRouter)', function () { + $completion = CreateResponse::from(chatCompletionOpenRouter(), meta()); + + expect($completion) + ->toBeInstanceOf(CreateResponse::class) + ->id->toBe('gen-123') + ->object->toBe('chat.completion') + ->created->toBe(1744873707) + ->model->toBe('mistral/ministral-8b') + ->systemFingerprint->toBeNull() + ->choices->toBeArray()->toHaveCount(1) + ->choices->each->toBeInstanceOf(CreateResponseChoice::class) + ->usage->toBeInstanceOf(CreateResponseUsage::class) + ->meta()->toBeInstanceOf(MetaInformation::class); +}); + +test('from (OpenRouter OpenAI)', function () { + $completion = CreateResponse::from(chatCompletionOpenRouterOpenAI(), meta()); + + expect($completion) + ->toBeInstanceOf(CreateResponse::class) + ->id->toBe('gen-123') + ->object->toBe('chat.completion') + ->created->toBe(1744900650) + ->model->toBe('openai/gpt-4o-mini') + ->systemFingerprint->toBe('fp_0392822090') + ->choices->toBeArray()->toHaveCount(1) + ->choices->each->toBeInstanceOf(CreateResponseChoice::class) + ->usage->toBeInstanceOf(CreateResponseUsage::class) + ->meta()->toBeInstanceOf(MetaInformation::class); +}); + +test('from (OpenRouter Google)', function () { + $completion = CreateResponse::from(chatCompletionOpenRouterGoogle(), meta()); + + expect($completion) + ->toBeInstanceOf(CreateResponse::class) + ->id->toBe('gen-123') + ->object->toBe('chat.completion') + ->created->toBe(1744910839) + ->model->toBe('google/gemini-2.5-pro-preview-03-25') + ->systemFingerprint->toBeNull() + ->choices->toBeArray()->toHaveCount(1) + ->choices->each->toBeInstanceOf(CreateResponseChoice::class) + ->usage->toBeInstanceOf(CreateResponseUsage::class) + ->meta()->toBeInstanceOf(MetaInformation::class); +}); + +test('from (OpenRouter xAI)', function () { + $completion = CreateResponse::from(chatCompletionOpenRouterXAI(), meta()); + + expect($completion) + ->toBeInstanceOf(CreateResponse::class) + ->id->toBe('gen-123') + ->object->toBe('chat.completion') + ->created->toBe(1744911228) + ->model->toBe('x-ai/grok-3-mini-beta') + ->systemFingerprint->toBe('fp_d133ae3397') + ->choices->toBeArray()->toHaveCount(1) + ->choices->each->toBeInstanceOf(CreateResponseChoice::class) + ->usage->toBeInstanceOf(CreateResponseUsage::class) + ->meta()->toBeInstanceOf(MetaInformation::class); +}); diff --git a/tests/Responses/Chat/CreateResponseChoice.php b/tests/Responses/Chat/CreateResponseChoice.php index af255e85..378d9656 100644 --- a/tests/Responses/Chat/CreateResponseChoice.php +++ b/tests/Responses/Chat/CreateResponseChoice.php @@ -44,6 +44,36 @@ ->finishReason->toBeNull(); }); +test('from OpenRouter OpenAI response', function () { + $result = CreateResponseChoice::from(chatCompletionOpenRouterOpenAI()['choices'][0]); + + expect($result) + ->index->toBe(0) + ->message->toBeInstanceOf(CreateResponseMessage::class) + ->logprobs->toBeNull() + ->finishReason->toBe('stop'); +}); + +test('from OpenRouter Google response', function () { + $result = CreateResponseChoice::from(chatCompletionOpenRouterGoogle()['choices'][0]); + + expect($result) + ->index->toBe(0) + ->message->toBeInstanceOf(CreateResponseMessage::class) + ->logprobs->toBeNull() + ->finishReason->toBe('stop'); +}); + +test('from OpenRouter xAI response', function () { + $result = CreateResponseChoice::from(chatCompletionOpenRouterXAI()['choices'][0]); + + expect($result) + ->index->toBe(0) + ->message->toBeInstanceOf(CreateResponseMessage::class) + ->logprobs->toBeNull() + ->finishReason->toBe('stop'); +}); + test('to array', function () { $result = CreateResponseChoice::from(chatCompletion()['choices'][0]); diff --git a/tests/Responses/Chat/CreateResponseUsage.php b/tests/Responses/Chat/CreateResponseUsage.php index 5f9b1606..06f298a5 100644 --- a/tests/Responses/Chat/CreateResponseUsage.php +++ b/tests/Responses/Chat/CreateResponseUsage.php @@ -15,6 +15,50 @@ ->completionTokensDetails->toBeInstanceOf(CreateResponseUsageCompletionTokensDetails::class); }); +test('from (OpenRouter)', function () { + $result = CreateResponseUsage::from(chatCompletionOpenRouter()['usage']); + + expect($result) + ->promptTokens->toBe(13) + ->completionTokens->toBe(20) + ->totalTokens->toBe(33) + ->promptTokensDetails->toBeNull() + ->completionTokensDetails->toBeNull(); +}); + +test('from (OpenRouter OpenAI)', function () { + $result = CreateResponseUsage::from(chatCompletionOpenRouterOpenAI()['usage']); + + expect($result) + ->promptTokens->toBe(21) + ->completionTokens->toBe(21) + ->totalTokens->toBe(42) + ->promptTokensDetails->toBeInstanceOf(CreateResponseUsagePromptTokensDetails::class) + ->completionTokensDetails->toBeInstanceOf(CreateResponseUsageCompletionTokensDetails::class); +}); + +test('from (OpenRouter Google)', function () { + $result = CreateResponseUsage::from(chatCompletionOpenRouterGoogle()['usage']); + + expect($result) + ->promptTokens->toBe(10) + ->completionTokens->toBe(138) + ->totalTokens->toBe(148) + ->promptTokensDetails->toBeNull() + ->completionTokensDetails->toBeNull(); +}); + +test('from (OpenRouter xAI)', function () { + $result = CreateResponseUsage::from(chatCompletionOpenRouterXAI()['usage']); + + expect($result) + ->promptTokens->toBe(21) + ->completionTokens->toBe(36) + ->totalTokens->toBe(392) + ->promptTokensDetails->toBeInstanceOf(CreateResponseUsagePromptTokensDetails::class) + ->completionTokensDetails->toBeInstanceOf(CreateResponseUsageCompletionTokensDetails::class); +}); + test('to array', function () { $result = CreateResponseUsage::from(chatCompletion()['usage']); From c22f1074c22b0ec3bfe01e92e778348ce12f4537 Mon Sep 17 00:00:00 2001 From: the-fermi-paradox <86816389+the-fermi-paradox@users.noreply.github.com> Date: Sun, 27 Apr 2025 06:13:02 -0500 Subject: [PATCH 22/29] fix(Gemini): correct list models call (#567) * fix: Adjusts Model\RetrieveResponse to handle Gemini API calls * fix: Adjusts PHPdoc to account for nullable 'created' field --- src/Responses/Models/ListResponse.php | 6 +++--- src/Responses/Models/RetrieveResponse.php | 10 +++++----- tests/Fixtures/Model.php | 12 ++++++++++++ tests/Responses/Models/RetrieveResponse.php | 11 +++++++++++ 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/Responses/Models/ListResponse.php b/src/Responses/Models/ListResponse.php index 598897a6..b460cf61 100644 --- a/src/Responses/Models/ListResponse.php +++ b/src/Responses/Models/ListResponse.php @@ -12,12 +12,12 @@ use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @implements ResponseContract}> + * @implements ResponseContract}> */ final class ListResponse implements ResponseContract, ResponseHasMetaInformationContract { /** - * @use ArrayAccessible}> + * @use ArrayAccessible}> */ use ArrayAccessible; @@ -36,7 +36,7 @@ private function __construct( /** * Acts as static factory, and returns a new Response instance. * - * @param array{object: string, data: array} $attributes + * @param array{object: string, data: array} $attributes */ public static function from(array $attributes, MetaInformation $meta): self { diff --git a/src/Responses/Models/RetrieveResponse.php b/src/Responses/Models/RetrieveResponse.php index b10e0d2c..b434c742 100644 --- a/src/Responses/Models/RetrieveResponse.php +++ b/src/Responses/Models/RetrieveResponse.php @@ -12,12 +12,12 @@ use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @implements ResponseContract + * @implements ResponseContract */ final class RetrieveResponse implements ResponseContract, ResponseHasMetaInformationContract { /** - * @use ArrayAccessible + * @use ArrayAccessible */ use ArrayAccessible; @@ -27,7 +27,7 @@ final class RetrieveResponse implements ResponseContract, ResponseHasMetaInforma private function __construct( public readonly string $id, public readonly string $object, - public readonly int $created, + public readonly ?int $created, public readonly string $ownedBy, private readonly MetaInformation $meta, ) {} @@ -35,14 +35,14 @@ private function __construct( /** * Acts as static factory, and returns a new Response instance. * - * @param array{id: string, object: string, created: int, owned_by: string} $attributes + * @param array{id: string, object: string, created: ?int, owned_by: string} $attributes */ public static function from(array $attributes, MetaInformation $meta): self { return new self( $attributes['id'], $attributes['object'], - $attributes['created'], + $attributes['created'] ?? null, $attributes['owned_by'], $meta, ); diff --git a/tests/Fixtures/Model.php b/tests/Fixtures/Model.php index b2f06f0b..19fa00ae 100644 --- a/tests/Fixtures/Model.php +++ b/tests/Fixtures/Model.php @@ -13,6 +13,18 @@ function model(): array ]; } +/** + * @return array + */ +function googleModel(): array +{ + return [ + 'id' => 'text-davinci-003', + 'object' => 'model', + 'owned_by' => 'google', + ]; +} + /** * @return array */ diff --git a/tests/Responses/Models/RetrieveResponse.php b/tests/Responses/Models/RetrieveResponse.php index 8ee72d2c..a4f63eda 100644 --- a/tests/Responses/Models/RetrieveResponse.php +++ b/tests/Responses/Models/RetrieveResponse.php @@ -15,6 +15,17 @@ ->meta()->toBeInstanceOf(MetaInformation::class); }); +test('from google', function () { + $result = RetrieveResponse::from(googleModel(), meta()); + + expect($result) + ->toBeInstanceOf(RetrieveResponse::class) + ->id->toBe('text-davinci-003') + ->object->toBe('model') + ->ownedBy->toBe('google') + ->meta()->toBeInstanceOf(MetaInformation::class); +}); + test('as array accessible', function () { $result = RetrieveResponse::from(model(), meta()); From d7e3238ed13b7a7e2fcf61414bdb861ed33b3516 Mon Sep 17 00:00:00 2001 From: Ayman Abi Aoun <90450422+abikali@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:15:29 +0300 Subject: [PATCH 23/29] feat(OpenAI): Add Image Response usage (#571) Co-authored-by: Ayman Abi Aoun --- src/Resources/Images.php | 6 +- src/Responses/Images/CreateResponse.php | 16 ++- src/Responses/Images/EditResponse.php | 16 ++- src/Responses/Images/ImageResponseUsage.php | 41 ++++++ .../ImageResponseUsageInputTokensDetails.php | 35 +++++ src/Responses/Images/VariationResponse.php | 16 ++- tests/Fixtures/Image.php | 72 +++++++++++ tests/Resources/Images.php | 120 +++++++++++++++++- tests/Responses/Images/CreateResponse.php | 34 ++++- tests/Responses/Images/EditResponse.php | 34 ++++- tests/Responses/Images/VariationResponse.php | 34 ++++- 11 files changed, 400 insertions(+), 24 deletions(-) create mode 100644 src/Responses/Images/ImageResponseUsage.php create mode 100644 src/Responses/Images/ImageResponseUsageInputTokensDetails.php diff --git a/src/Resources/Images.php b/src/Resources/Images.php index 465678c4..2aa73a67 100644 --- a/src/Resources/Images.php +++ b/src/Resources/Images.php @@ -26,7 +26,7 @@ public function create(array $parameters): CreateResponse { $payload = Payload::create('images/generations', $parameters); - /** @var Response}> $response */ + /** @var Response, usage?: array{total_tokens: int, input_tokens: int, output_tokens: int, input_tokens_details: array{text_tokens: int, image_tokens: int}}}> $response */ $response = $this->transporter->requestObject($payload); return CreateResponse::from($response->data(), $response->meta()); @@ -43,7 +43,7 @@ public function edit(array $parameters): EditResponse { $payload = Payload::upload('images/edits', $parameters); - /** @var Response}> $response */ + /** @var Response, usage?: array{total_tokens: int, input_tokens: int, output_tokens: int, input_tokens_details: array{text_tokens: int, image_tokens: int}}}> $response */ $response = $this->transporter->requestObject($payload); return EditResponse::from($response->data(), $response->meta()); @@ -60,7 +60,7 @@ public function variation(array $parameters): VariationResponse { $payload = Payload::upload('images/variations', $parameters); - /** @var Response}> $response */ + /** @var Response, usage?: array{total_tokens: int, input_tokens: int, output_tokens: int, input_tokens_details: array{text_tokens: int, image_tokens: int}}}> $response */ $response = $this->transporter->requestObject($payload); return VariationResponse::from($response->data(), $response->meta()); diff --git a/src/Responses/Images/CreateResponse.php b/src/Responses/Images/CreateResponse.php index edb9c4d6..9d94d51b 100644 --- a/src/Responses/Images/CreateResponse.php +++ b/src/Responses/Images/CreateResponse.php @@ -12,12 +12,12 @@ use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @implements ResponseContract}> + * @implements ResponseContract, usage?: array{total_tokens: int, input_tokens: int, output_tokens: int, input_tokens_details: array{text_tokens: int, image_tokens: int}}}> */ final class CreateResponse implements ResponseContract, ResponseHasMetaInformationContract { /** - * @use ArrayAccessible}> + * @use ArrayAccessible, usage?: array{total_tokens: int, input_tokens: int, output_tokens: int, input_tokens_details: array{text_tokens: int, image_tokens: int}}}> */ use ArrayAccessible; @@ -31,12 +31,13 @@ private function __construct( public readonly int $created, public readonly array $data, private readonly MetaInformation $meta, + public readonly ?ImageResponseUsage $usage = null, ) {} /** * Acts as static factory, and returns a new Response instance. * - * @param array{created: int, data: array} $attributes + * @param array{created: int, data: array, usage?: array{total_tokens: int, input_tokens: int, output_tokens: int, input_tokens_details: array{text_tokens: int, image_tokens: int}}} $attributes */ public static function from(array $attributes, MetaInformation $meta): self { @@ -48,6 +49,7 @@ public static function from(array $attributes, MetaInformation $meta): self $attributes['created'], $results, $meta, + isset($attributes['usage']) ? ImageResponseUsage::from($attributes['usage']) : null, ); } @@ -56,12 +58,18 @@ public static function from(array $attributes, MetaInformation $meta): self */ public function toArray(): array { - return [ + $result = [ 'created' => $this->created, 'data' => array_map( static fn (CreateResponseData $result): array => $result->toArray(), $this->data, ), ]; + + if ($this->usage !== null) { + $result['usage'] = $this->usage->toArray(); + } + + return $result; } } diff --git a/src/Responses/Images/EditResponse.php b/src/Responses/Images/EditResponse.php index 8f044880..23cbb0f7 100644 --- a/src/Responses/Images/EditResponse.php +++ b/src/Responses/Images/EditResponse.php @@ -12,12 +12,12 @@ use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @implements ResponseContract}> + * @implements ResponseContract, usage?: array{total_tokens: int, input_tokens: int, output_tokens: int, input_tokens_details: array{text_tokens: int, image_tokens: int}}}> */ final class EditResponse implements ResponseContract, ResponseHasMetaInformationContract { /** - * @use ArrayAccessible}> + * @use ArrayAccessible, usage?: array{total_tokens: int, input_tokens: int, output_tokens: int, input_tokens_details: array{text_tokens: int, image_tokens: int}}}> */ use ArrayAccessible; @@ -31,12 +31,13 @@ private function __construct( public readonly int $created, public readonly array $data, private readonly MetaInformation $meta, + public readonly ?ImageResponseUsage $usage = null, ) {} /** * Acts as static factory, and returns a new Response instance. * - * @param array{created: int, data: array} $attributes + * @param array{created: int, data: array, usage?: array{total_tokens: int, input_tokens: int, output_tokens: int, input_tokens_details: array{text_tokens: int, image_tokens: int}}} $attributes */ public static function from(array $attributes, MetaInformation $meta): self { @@ -48,6 +49,7 @@ public static function from(array $attributes, MetaInformation $meta): self $attributes['created'], $results, $meta, + isset($attributes['usage']) ? ImageResponseUsage::from($attributes['usage']) : null, ); } @@ -56,12 +58,18 @@ public static function from(array $attributes, MetaInformation $meta): self */ public function toArray(): array { - return [ + $result = [ 'created' => $this->created, 'data' => array_map( static fn (EditResponseData $result): array => $result->toArray(), $this->data, ), ]; + + if ($this->usage !== null) { + $result['usage'] = $this->usage->toArray(); + } + + return $result; } } diff --git a/src/Responses/Images/ImageResponseUsage.php b/src/Responses/Images/ImageResponseUsage.php new file mode 100644 index 00000000..7e39acde --- /dev/null +++ b/src/Responses/Images/ImageResponseUsage.php @@ -0,0 +1,41 @@ + $this->totalTokens, + 'input_tokens' => $this->inputTokens, + 'output_tokens' => $this->outputTokens, + 'input_tokens_details' => $this->inputTokensDetails->toArray(), + ]; + } +} diff --git a/src/Responses/Images/ImageResponseUsageInputTokensDetails.php b/src/Responses/Images/ImageResponseUsageInputTokensDetails.php new file mode 100644 index 00000000..144857cf --- /dev/null +++ b/src/Responses/Images/ImageResponseUsageInputTokensDetails.php @@ -0,0 +1,35 @@ + $this->textTokens, + 'image_tokens' => $this->imageTokens, + ]; + } +} diff --git a/src/Responses/Images/VariationResponse.php b/src/Responses/Images/VariationResponse.php index deee6652..b2bb1eb5 100644 --- a/src/Responses/Images/VariationResponse.php +++ b/src/Responses/Images/VariationResponse.php @@ -12,12 +12,12 @@ use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @implements ResponseContract}> + * @implements ResponseContract, usage?: array{total_tokens: int, input_tokens: int, output_tokens: int, input_tokens_details: array{text_tokens: int, image_tokens: int}}}> */ final class VariationResponse implements ResponseContract, ResponseHasMetaInformationContract { /** - * @use ArrayAccessible}> + * @use ArrayAccessible, usage?: array{total_tokens: int, input_tokens: int, output_tokens: int, input_tokens_details: array{text_tokens: int, image_tokens: int}}}> */ use ArrayAccessible; @@ -31,12 +31,13 @@ private function __construct( public readonly int $created, public readonly array $data, private readonly MetaInformation $meta, + public readonly ?ImageResponseUsage $usage = null, ) {} /** * Acts as static factory, and returns a new Response instance. * - * @param array{created: int, data: array} $attributes + * @param array{created: int, data: array, usage?: array{total_tokens: int, input_tokens: int, output_tokens: int, input_tokens_details: array{text_tokens: int, image_tokens: int}}} $attributes */ public static function from(array $attributes, MetaInformation $meta): self { @@ -48,6 +49,7 @@ public static function from(array $attributes, MetaInformation $meta): self $attributes['created'], $results, $meta, + isset($attributes['usage']) ? ImageResponseUsage::from($attributes['usage']) : null, ); } @@ -56,12 +58,18 @@ public static function from(array $attributes, MetaInformation $meta): self */ public function toArray(): array { - return [ + $result = [ 'created' => $this->created, 'data' => array_map( static fn (VariationResponseData $result): array => $result->toArray(), $this->data, ), ]; + + if ($this->usage !== null) { + $result['usage'] = $this->usage->toArray(); + } + + return $result; } } diff --git a/tests/Fixtures/Image.php b/tests/Fixtures/Image.php index 95e3d8de..8768cf85 100644 --- a/tests/Fixtures/Image.php +++ b/tests/Fixtures/Image.php @@ -46,6 +46,30 @@ function imageCreateWithB46Json(): array ]; } +/** + * @return array + */ +function imageCreateWithUsage(): array +{ + return [ + 'created' => 1664136088, + 'data' => [ + [ + 'url' => 'https://openai.com/image.png', + ], + ], + 'usage' => [ + 'total_tokens' => 100, + 'input_tokens' => 50, + 'output_tokens' => 50, + 'input_tokens_details' => [ + 'text_tokens' => 10, + 'image_tokens' => 40, + ], + ], + ]; +} + /** * @return array */ @@ -76,6 +100,30 @@ function imageEditWithB46Json(): array ]; } +/** + * @return array + */ +function imageEditWithUsage(): array +{ + return [ + 'created' => 1664136088, + 'data' => [ + [ + 'url' => 'https://openai.com/image.png', + ], + ], + 'usage' => [ + 'total_tokens' => 100, + 'input_tokens' => 50, + 'output_tokens' => 50, + 'input_tokens_details' => [ + 'text_tokens' => 10, + 'image_tokens' => 40, + ], + ], + ]; +} + /** * @return array */ @@ -105,3 +153,27 @@ function imageVariationWithB46Json(): array ], ]; } + +/** + * @return array + */ +function imageVariationWithUsage(): array +{ + return [ + 'created' => 1664136088, + 'data' => [ + [ + 'url' => 'https://openai.com/image.png', + ], + ], + 'usage' => [ + 'total_tokens' => 100, + 'input_tokens' => 50, + 'output_tokens' => 50, + 'input_tokens_details' => [ + 'text_tokens' => 10, + 'image_tokens' => 40, + ], + ], + ]; +} diff --git a/tests/Resources/Images.php b/tests/Resources/Images.php index b8740020..a09959b5 100644 --- a/tests/Resources/Images.php +++ b/tests/Resources/Images.php @@ -4,6 +4,8 @@ use OpenAI\Responses\Images\CreateResponseData; use OpenAI\Responses\Images\EditResponse; use OpenAI\Responses\Images\EditResponseData; +use OpenAI\Responses\Images\ImageResponseUsage; +use OpenAI\Responses\Images\ImageResponseUsageInputTokensDetails; use OpenAI\Responses\Images\VariationResponse; use OpenAI\Responses\Images\VariationResponseData; use OpenAI\Responses\Meta\MetaInformation; @@ -27,7 +29,43 @@ ->toBeInstanceOf(CreateResponse::class) ->created->toBe(1664136088) ->data->toBeArray()->toHaveCount(1) - ->data->each->toBeInstanceOf(CreateResponseData::class); + ->data->each->toBeInstanceOf(CreateResponseData::class) + ->usage->toBeNull(); + + expect($result->data[0]) + ->url->toBe('https://openai.com/image.png'); + + expect($result->meta()) + ->toBeInstanceOf(MetaInformation::class); +}); + +test('create with usage', function () { + $client = mockClient('POST', 'images/generations', [ + 'prompt' => 'A cute baby sea otter', + 'n' => 1, + 'size' => '256x256', + 'response_format' => 'url', + ], \OpenAI\ValueObjects\Transporter\Response::from(imageCreateWithUsage(), metaHeaders())); + + $result = $client->images()->create([ + 'prompt' => 'A cute baby sea otter', + 'n' => 1, + 'size' => '256x256', + 'response_format' => 'url', + ]); + + expect($result) + ->toBeInstanceOf(CreateResponse::class) + ->created->toBe(1664136088) + ->data->toBeArray()->toHaveCount(1) + ->data->each->toBeInstanceOf(CreateResponseData::class) + ->usage->toBeInstanceOf(ImageResponseUsage::class) + ->usage->totalTokens->toBe(100) + ->usage->inputTokens->toBe(50) + ->usage->outputTokens->toBe(50) + ->usage->inputTokensDetails->toBeInstanceOf(ImageResponseUsageInputTokensDetails::class) + ->usage->inputTokensDetails->textTokens->toBe(10) + ->usage->inputTokensDetails->imageTokens->toBe(40); expect($result->data[0]) ->url->toBe('https://openai.com/image.png'); @@ -59,7 +97,47 @@ ->toBeInstanceOf(EditResponse::class) ->created->toBe(1664136088) ->data->toBeArray()->toHaveCount(1) - ->data->each->toBeInstanceOf(EditResponseData::class); + ->data->each->toBeInstanceOf(EditResponseData::class) + ->usage->toBeNull(); + + expect($result->data[0]) + ->url->toBe('https://openai.com/image.png'); + + expect($result->meta()) + ->toBeInstanceOf(MetaInformation::class); +}); + +test('edit with usage', function () { + $client = mockClient('POST', 'images/edits', [ + 'image' => fileResourceResource(), + 'mask' => fileResourceResource(), + 'prompt' => 'A sunlit indoor lounge area with a pool containing a flamingo', + 'n' => 1, + 'size' => '256x256', + 'response_format' => 'url', + ], \OpenAI\ValueObjects\Transporter\Response::from(imageEditWithUsage(), metaHeaders()), validateParams: false); + + $result = $client->images()->edit([ + 'image' => fileResourceResource(), + 'mask' => fileResourceResource(), + 'prompt' => 'A sunlit indoor lounge area with a pool containing a flamingo', + 'n' => 1, + 'size' => '256x256', + 'response_format' => 'url', + ]); + + expect($result) + ->toBeInstanceOf(EditResponse::class) + ->created->toBe(1664136088) + ->data->toBeArray()->toHaveCount(1) + ->data->each->toBeInstanceOf(EditResponseData::class) + ->usage->toBeInstanceOf(ImageResponseUsage::class) + ->usage->totalTokens->toBe(100) + ->usage->inputTokens->toBe(50) + ->usage->outputTokens->toBe(50) + ->usage->inputTokensDetails->toBeInstanceOf(ImageResponseUsageInputTokensDetails::class) + ->usage->inputTokensDetails->textTokens->toBe(10) + ->usage->inputTokensDetails->imageTokens->toBe(40); expect($result->data[0]) ->url->toBe('https://openai.com/image.png'); @@ -87,7 +165,43 @@ ->toBeInstanceOf(VariationResponse::class) ->created->toBe(1664136088) ->data->toBeArray()->toHaveCount(1) - ->data->each->toBeInstanceOf(VariationResponseData::class); + ->data->each->toBeInstanceOf(VariationResponseData::class) + ->usage->toBeNull(); + + expect($result->data[0]) + ->url->toBe('https://openai.com/image.png'); + + expect($result->meta()) + ->toBeInstanceOf(MetaInformation::class); +}); + +test('variation with usage', function () { + $client = mockClient('POST', 'images/variations', [ + 'image' => fileResourceResource(), + 'n' => 1, + 'size' => '256x256', + 'response_format' => 'url', + ], \OpenAI\ValueObjects\Transporter\Response::from(imageVariationWithUsage(), metaHeaders()), validateParams: false); + + $result = $client->images()->variation([ + 'image' => fileResourceResource(), + 'n' => 1, + 'size' => '256x256', + 'response_format' => 'url', + ]); + + expect($result) + ->toBeInstanceOf(VariationResponse::class) + ->created->toBe(1664136088) + ->data->toBeArray()->toHaveCount(1) + ->data->each->toBeInstanceOf(VariationResponseData::class) + ->usage->toBeInstanceOf(ImageResponseUsage::class) + ->usage->totalTokens->toBe(100) + ->usage->inputTokens->toBe(50) + ->usage->outputTokens->toBe(50) + ->usage->inputTokensDetails->toBeInstanceOf(ImageResponseUsageInputTokensDetails::class) + ->usage->inputTokensDetails->textTokens->toBe(10) + ->usage->inputTokensDetails->imageTokens->toBe(40); expect($result->data[0]) ->url->toBe('https://openai.com/image.png'); diff --git a/tests/Responses/Images/CreateResponse.php b/tests/Responses/Images/CreateResponse.php index 6ff3bd59..4e6ff751 100644 --- a/tests/Responses/Images/CreateResponse.php +++ b/tests/Responses/Images/CreateResponse.php @@ -2,6 +2,8 @@ use OpenAI\Responses\Images\CreateResponse; use OpenAI\Responses\Images\CreateResponseData; +use OpenAI\Responses\Images\ImageResponseUsage; +use OpenAI\Responses\Images\ImageResponseUsageInputTokensDetails; use OpenAI\Responses\Meta\MetaInformation; test('from with url', function () { @@ -12,7 +14,8 @@ ->created->toBe(1664136088) ->data->toBeArray()->toHaveCount(1) ->data->each->toBeInstanceOf(CreateResponseData::class) - ->meta()->toBeInstanceOf(MetaInformation::class); + ->meta()->toBeInstanceOf(MetaInformation::class) + ->usage->toBeNull(); }); test('as array accessible with url', function () { @@ -37,7 +40,8 @@ ->created->toBe(1664136088) ->data->toBeArray()->toHaveCount(1) ->data->each->toBeInstanceOf(CreateResponseData::class) - ->meta()->toBeInstanceOf(MetaInformation::class); + ->meta()->toBeInstanceOf(MetaInformation::class) + ->usage->toBeNull(); }); test('as array accessible with b64_json', function () { @@ -54,6 +58,32 @@ ->toBe(imageCreateWithB46Json()); }); +test('from with usage', function () { + $response = CreateResponse::from(imageCreateWithUsage(), meta()); + + expect($response) + ->toBeInstanceOf(CreateResponse::class) + ->created->toBe(1664136088) + ->data->toBeArray()->toHaveCount(1) + ->data->each->toBeInstanceOf(CreateResponseData::class) + ->meta()->toBeInstanceOf(MetaInformation::class) + ->usage->toBeInstanceOf(ImageResponseUsage::class) + ->usage->totalTokens->toBe(100) + ->usage->inputTokens->toBe(50) + ->usage->outputTokens->toBe(50) + ->usage->inputTokensDetails->toBeInstanceOf(ImageResponseUsageInputTokensDetails::class) + ->usage->inputTokensDetails->textTokens->toBe(10) + ->usage->inputTokensDetails->imageTokens->toBe(40); +}); + +test('to array with usage', function () { + $response = CreateResponse::from(imageCreateWithUsage(), meta()); + + expect($response->toArray()) + ->toBeArray() + ->toBe(imageCreateWithUsage()); +}); + test('fake', function () { $response = CreateResponse::fake(); diff --git a/tests/Responses/Images/EditResponse.php b/tests/Responses/Images/EditResponse.php index 0530b6b8..09861079 100644 --- a/tests/Responses/Images/EditResponse.php +++ b/tests/Responses/Images/EditResponse.php @@ -2,6 +2,8 @@ use OpenAI\Responses\Images\EditResponse; use OpenAI\Responses\Images\EditResponseData; +use OpenAI\Responses\Images\ImageResponseUsage; +use OpenAI\Responses\Images\ImageResponseUsageInputTokensDetails; use OpenAI\Responses\Meta\MetaInformation; test('from with url', function () { @@ -12,7 +14,8 @@ ->created->toBe(1664136088) ->data->toBeArray()->toHaveCount(1) ->data->each->toBeInstanceOf(EditResponseData::class) - ->meta()->toBeInstanceOf(MetaInformation::class); + ->meta()->toBeInstanceOf(MetaInformation::class) + ->usage->toBeNull(); }); test('as array accessible with url', function () { @@ -37,7 +40,8 @@ ->created->toBe(1664136088) ->data->toBeArray()->toHaveCount(1) ->data->each->toBeInstanceOf(EditResponseData::class) - ->meta()->toBeInstanceOf(MetaInformation::class); + ->meta()->toBeInstanceOf(MetaInformation::class) + ->usage->toBeNull(); }); test('as array accessible with b64_json', function () { @@ -54,6 +58,32 @@ ->toBe(imageEditWithB46Json()); }); +test('from with usage', function () { + $response = EditResponse::from(imageEditWithUsage(), meta()); + + expect($response) + ->toBeInstanceOf(EditResponse::class) + ->created->toBe(1664136088) + ->data->toBeArray()->toHaveCount(1) + ->data->each->toBeInstanceOf(EditResponseData::class) + ->meta()->toBeInstanceOf(MetaInformation::class) + ->usage->toBeInstanceOf(ImageResponseUsage::class) + ->usage->totalTokens->toBe(100) + ->usage->inputTokens->toBe(50) + ->usage->outputTokens->toBe(50) + ->usage->inputTokensDetails->toBeInstanceOf(ImageResponseUsageInputTokensDetails::class) + ->usage->inputTokensDetails->textTokens->toBe(10) + ->usage->inputTokensDetails->imageTokens->toBe(40); +}); + +test('to array with usage', function () { + $response = EditResponse::from(imageEditWithUsage(), meta()); + + expect($response->toArray()) + ->toBeArray() + ->toBe(imageEditWithUsage()); +}); + test('fake', function () { $response = EditResponse::fake(); diff --git a/tests/Responses/Images/VariationResponse.php b/tests/Responses/Images/VariationResponse.php index a66a5690..4233afd4 100644 --- a/tests/Responses/Images/VariationResponse.php +++ b/tests/Responses/Images/VariationResponse.php @@ -1,5 +1,7 @@ created->toBe(1664136088) ->data->toBeArray()->toHaveCount(1) ->data->each->toBeInstanceOf(VariationResponseData::class) - ->meta()->toBeInstanceOf(MetaInformation::class); + ->meta()->toBeInstanceOf(MetaInformation::class) + ->usage->toBeNull(); }); test('as array accessible with url', function () { @@ -37,7 +40,8 @@ ->created->toBe(1664136088) ->data->toBeArray()->toHaveCount(1) ->data->each->toBeInstanceOf(VariationResponseData::class) - ->meta()->toBeInstanceOf(MetaInformation::class); + ->meta()->toBeInstanceOf(MetaInformation::class) + ->usage->toBeNull(); }); test('as array accessible with b64_json', function () { @@ -54,6 +58,32 @@ ->toBe(imageVariationWithB46Json()); }); +test('from with usage', function () { + $response = VariationResponse::from(imageVariationWithUsage(), meta()); + + expect($response) + ->toBeInstanceOf(VariationResponse::class) + ->created->toBe(1664136088) + ->data->toBeArray()->toHaveCount(1) + ->data->each->toBeInstanceOf(VariationResponseData::class) + ->meta()->toBeInstanceOf(MetaInformation::class) + ->usage->toBeInstanceOf(ImageResponseUsage::class) + ->usage->totalTokens->toBe(100) + ->usage->inputTokens->toBe(50) + ->usage->outputTokens->toBe(50) + ->usage->inputTokensDetails->toBeInstanceOf(ImageResponseUsageInputTokensDetails::class) + ->usage->inputTokensDetails->textTokens->toBe(10) + ->usage->inputTokensDetails->imageTokens->toBe(40); +}); + +test('to array with usage', function () { + $response = VariationResponse::from(imageVariationWithUsage(), meta()); + + expect($response->toArray()) + ->toBeArray() + ->toBe(imageVariationWithUsage()); +}); + test('fake', function () { $response = VariationResponse::fake(); From 62e9822c20eae25145628f337bbc38679badbab4 Mon Sep 17 00:00:00 2001 From: Makenson Petit-Fils Clervil Date: Wed, 30 Apr 2025 08:43:05 -0600 Subject: [PATCH 24/29] feat(OpenAI): Add category applied input types to moderation response (#572) * Add support for multi-modal moderation inputs and category applied input types * put parameter documentation in single line * Add support for omni moderation with text and image inputs * put DocBlock in single line * chore: remove unneeded docblock changes * chore: unneeded spacing change * chore: pint --------- Co-authored-by: Connor Tumbleson --- .../Moderations/CategoryAppliedInputType.php | 12 +++ src/Resources/Moderations.php | 2 +- src/Responses/Moderations/CreateResponse.php | 6 +- .../Moderations/CreateResponseResult.php | 17 ++++- tests/Fixtures/Moderation.php | 76 +++++++++++++++++++ tests/Resources/Moderations.php | 71 ++++++++++++++++- .../Resources/ModerationsTestResource.php | 31 ++++++++ 7 files changed, 204 insertions(+), 11 deletions(-) create mode 100644 src/Enums/Moderations/CategoryAppliedInputType.php diff --git a/src/Enums/Moderations/CategoryAppliedInputType.php b/src/Enums/Moderations/CategoryAppliedInputType.php new file mode 100644 index 00000000..e300abae --- /dev/null +++ b/src/Enums/Moderations/CategoryAppliedInputType.php @@ -0,0 +1,12 @@ +, category_scores: array, flagged: bool}>}> $response */ + /** @var Response, category_scores: array, flagged: bool,category_applied_input_types?: array>}>}> $response */ $response = $this->transporter->requestObject($payload); return CreateResponse::from($response->data(), $response->meta()); diff --git a/src/Responses/Moderations/CreateResponse.php b/src/Responses/Moderations/CreateResponse.php index 7815ca50..369f1b0d 100644 --- a/src/Responses/Moderations/CreateResponse.php +++ b/src/Responses/Moderations/CreateResponse.php @@ -12,12 +12,12 @@ use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @implements ResponseContract, category_scores: array, flagged: bool}>}> + * @implements ResponseContract, category_scores: array, flagged: bool, category_applied_input_types?: array>}>}> */ final class CreateResponse implements ResponseContract, ResponseHasMetaInformationContract { /** - * @use ArrayAccessible, category_scores: array, flagged: bool}>}> + * @use ArrayAccessible, category_scores: array, flagged: bool, category_applied_input_types?: array>}>}> */ use ArrayAccessible; @@ -37,7 +37,7 @@ private function __construct( /** * Acts as static factory, and returns a new Response instance. * - * @param array{id: string, model: string, results: array, category_scores: array, flagged: bool}>} $attributes + * @param array{id: string, model: string, results: array, category_scores: array, flagged: bool, category_applied_input_types?: array>}>} $attributes */ public static function from(array $attributes, MetaInformation $meta): self { diff --git a/src/Responses/Moderations/CreateResponseResult.php b/src/Responses/Moderations/CreateResponseResult.php index a913dd75..9687b040 100644 --- a/src/Responses/Moderations/CreateResponseResult.php +++ b/src/Responses/Moderations/CreateResponseResult.php @@ -10,16 +10,18 @@ final class CreateResponseResult { /** * @param array $categories + * @param array> $categoryAppliedInputTypes */ private function __construct( public readonly array $categories, public readonly bool $flagged, + public readonly ?array $categoryAppliedInputTypes, ) { // .. } /** - * @param array{categories: array, category_scores: array, flagged: bool} $attributes + * @param array{categories: array, category_scores: array, flagged: bool, category_applied_input_types?: array>} $attributes */ public static function from(array $attributes): self { @@ -40,12 +42,13 @@ public static function from(array $attributes): self return new CreateResponseResult( $categories, - $attributes['flagged'] + $attributes['flagged'], + $attributes['category_applied_input_types'] ?? null, ); } /** - * @return array{categories: array, category_scores: array, flagged: bool} + * @return array{ categories: array, category_scores: array, flagged: bool, category_applied_input_types?: array>} */ public function toArray(): array { @@ -56,10 +59,16 @@ public function toArray(): array $categoryScores[$category->category->value] = $category->score; } - return [ + $result = [ 'categories' => $categories, 'category_scores' => $categoryScores, 'flagged' => $this->flagged, ]; + + if ($this->categoryAppliedInputTypes !== null) { + $result['category_applied_input_types'] = $this->categoryAppliedInputTypes; + } + + return $result; } } diff --git a/tests/Fixtures/Moderation.php b/tests/Fixtures/Moderation.php index 9b7c2ab2..b1ca93fa 100644 --- a/tests/Fixtures/Moderation.php +++ b/tests/Fixtures/Moderation.php @@ -83,6 +83,82 @@ function moderationOmniResource(): array 'violence/graphic' => 0.036865197122097015, ], 'flagged' => true, + 'category_applied_input_types' => [ + 'hate' => ['text'], + 'hate/threatening' => ['text'], + 'harassment' => ['text'], + 'harassment/threatening' => ['text'], + 'self-harm' => ['text'], + 'self-harm/intent' => ['text'], + 'self-harm/instructions' => ['text'], + 'sexual' => ['text'], + 'sexual/minors' => ['text'], + 'violence' => ['text'], + 'violence/graphic' => ['text'], + 'illicit' => ['text'], + 'illicit/violent' => ['text'], + ], + ], + ], + ]; +} + +/** + * @return array + */ +function moderationOmniWithTextAndImageResource(): array +{ + return [ + 'id' => 'modr-5MWoLO', + 'model' => 'omni-moderation-001', + 'results' => [ + [ + 'categories' => [ + 'hate' => false, + 'hate/threatening' => false, + 'harassment' => false, + 'harassment/threatening' => false, + 'illicit' => false, + 'illicit/violent' => false, + 'self-harm' => false, + 'self-harm/intent' => false, + 'self-harm/instructions' => false, + 'sexual' => false, + 'sexual/minors' => false, + 'violence' => true, + 'violence/graphic' => true, + ], + 'category_scores' => [ + 'hate' => 0.22714105248451233, + 'hate/threatening' => 0.4132447838783264, + 'illicit' => 0.1602763684674149, + 'illicit/violent' => 0.9223177433013916, + 'harassment' => 0.1602763684674149, + 'harassment/threatening' => 0.1602763684674149, + 'self-harm' => 0.005232391878962517, + 'self-harm/intent' => 0.005134391873962517, + 'self-harm/instructions' => 0.005132591874962517, + 'sexual' => 0.01407341007143259, + 'sexual/minors' => 0.0038522258400917053, + 'violence' => 0.4132447838783264, + 'violence/graphic' => 5.7929166992142E-5, + ], + 'flagged' => true, + 'category_applied_input_types' => [ + 'hate' => ['text'], + 'hate/threatening' => ['text'], + 'harassment' => ['text'], + 'harassment/threatening' => ['text'], + 'self-harm' => ['text', 'image'], + 'self-harm/intent' => ['text', 'image'], + 'self-harm/instructions' => ['text', 'image'], + 'sexual' => ['text', 'image'], + 'sexual/minors' => ['text', 'image'], + 'violence' => ['text', 'image'], + 'violence/graphic' => ['text', 'image'], + 'illicit' => ['text'], + 'illicit/violent' => ['text'], + ], ], ], ]; diff --git a/tests/Resources/Moderations.php b/tests/Resources/Moderations.php index 7e5dff91..e531f981 100644 --- a/tests/Resources/Moderations.php +++ b/tests/Resources/Moderations.php @@ -1,12 +1,20 @@ [ + ['type' => 'text', 'text' => 'I love to kill...'], + ], + 'basic_text' => 'I want to kill them.', +]); + test('create legacy', closure: function () { $client = mockClient('POST', 'moderations', [ 'model' => 'text-moderation-latest', @@ -44,15 +52,15 @@ ->toBeInstanceOf(MetaInformation::class); }); -test('create omni', closure: function () { +test('create omni', closure: function ($input) { $client = mockClient('POST', 'moderations', [ 'model' => 'omni-moderation-latest', - 'input' => 'I want to kill them.', + 'input' => $input, ], Response::from(moderationOmniResource(), metaHeaders())); $result = $client->moderations()->create([ 'model' => 'omni-moderation-latest', - 'input' => 'I want to kill them.', + 'input' => $input, ]); expect($result) @@ -77,6 +85,63 @@ ->violated->toBe(true) ->score->toBe(0.9223177433013916); + expect($result->results[0]->categoryAppliedInputTypes) + ->toHaveCount(13) + ->each->toBe([CategoryAppliedInputType::Text->value]); + expect($result->meta()) ->toBeInstanceOf(MetaInformation::class); +})->with('create omni inputs'); + +test('create omni with image and text', closure: function () { + $client = mockClient('POST', 'moderations', [ + 'model' => 'omni-moderation-latest', + 'input' => [ + ['type' => 'text', 'text' => '.. I want to kill...'], + [ + 'type' => 'image_url', + 'image_url' => [ + 'url' => 'https://example.com/image.png', + ], + ], + ], + ], Response::from(moderationOmniWithTextAndImageResource(), metaHeaders())); + + $result = $client->moderations()->create([ + 'model' => 'omni-moderation-latest', + 'input' => [ + ['type' => 'text', 'text' => '.. I want to kill...'], + [ + 'type' => 'image_url', + 'image_url' => [ + 'url' => 'https://example.com/image.png', + ], + ], + ], + ]); + + expect($result) + ->toBeInstanceOf(CreateResponse::class) + ->id->toBe('modr-5MWoLO') + ->model->toBe('omni-moderation-001') + ->results->toBeArray()->toHaveCount(1) + ->results->each->toBeInstanceOf(CreateResponseResult::class); + + expect($result->results[0]) + ->flagged->toBeTrue() + ->categories->toHaveCount(13) + ->each->toBeInstanceOf(CreateResponseCategory::class) + ->categoryAppliedInputTypes->toHaveCount(13); + + expect($result->results[0]->categories[Category::ViolenceGraphic->value]) + ->category->toBe(Category::ViolenceGraphic) + ->violated->toBe(true) + ->score->toBe(5.7929166992142E-5); + + expect($result->results[0]->categoryAppliedInputTypes[Category::IllicitViolent->value]) + ->toBe([CategoryAppliedInputType::Text->value]); + + expect($result->results[0]->categoryAppliedInputTypes[Category::ViolenceGraphic->value]) + ->toBe([CategoryAppliedInputType::Text->value, CategoryAppliedInputType::Image->value]); + }); diff --git a/tests/Testing/Resources/ModerationsTestResource.php b/tests/Testing/Resources/ModerationsTestResource.php index d2c0709c..a86a64a8 100644 --- a/tests/Testing/Resources/ModerationsTestResource.php +++ b/tests/Testing/Resources/ModerationsTestResource.php @@ -20,3 +20,34 @@ $parameters['input'] === 'I want to k*** them.'; }); }); + +it('records a multi-modal moderations create request', function () { + $fake = new ClientFake([ + CreateResponse::fake(), + ]); + + $fake->moderations()->create([ + 'model' => 'text-moderation-omni', + 'input' => [ + [ + 'type' => 'text', + 'text' => 'I want to k*** them.', + ], + [ + 'type' => 'image_url', + 'image_url' => [ + 'url' => 'https://example.com/potentially-harmful-image.jpg', + ], + ], + ], + ]); + + $fake->assertSent(Moderations::class, function ($method, $parameters) { + return $method === 'create' && + $parameters['model'] === 'text-moderation-omni' && + $parameters['input'][0]['type'] === 'text' && + $parameters['input'][0]['text'] === 'I want to k*** them.' && + $parameters['input'][1]['type'] === 'image_url' && + $parameters['input'][1]['image_url']['url'] === 'https://example.com/potentially-harmful-image.jpg'; + }); +}); From 9bd5a25b8f9c0ebe5926fd74556b89b9d0a7a362 Mon Sep 17 00:00:00 2001 From: Sarah Riley <89945680+Saraphoo@users.noreply.github.com> Date: Thu, 1 May 2025 08:36:32 -0400 Subject: [PATCH 25/29] feat(OpenAI): Support annotations in Chat response. (Web Search) (#564) * Planning comments * Planning notes * Planning notes * add support for web_search_options * Add classes for Response Choice web_search_options * web_search_options classes to final classes * Add web_search_options to chat fixtures * update docblock and make web_search_option nullable in return * expect webSearchOptions * Return webSearchOptionContent with content size and user location * Update dockblocks and add type approximate * add test for Response Choice WebSearchOptions classes * fix docblock * Delete package-lock.json * Return annotations * Test fixtures for annotations * Annotations instead of Web_search_options WIP * Remove unneeded class * Update doc blocks * Update doc block * CreateResponseChoiceAnnoations class * CreateResponseChoiceAnnotaionUrlCitations class * Delete .gitignore * git-ignore * Restore .gitignore file * test createResponseChoiceAnnotations * chatCompletionWithAnnotations in correct format * CreateResponseChoice test in correct format * better from method and return expected type of url_citation * better testing data for WithAnnotations * Test CreateResponseChoiceAnnotationsUrlCitationsTest.php * return url_citation items in the correct order * Pint * Move annotations out of createResponseChoice * Move annotations into createResponseMessage * Annotations in message for chatCompletionWithAnnotations * Update test for CreateResponseChoiceAnnotations to pass * CreateResponseChoiceAnnotations returns annotations array * Correct doc block for toArray on choice Annotations * WIP collect annotations array for Response Message * Return annotations from createResponseChoiceAnnotations class to response message correctly * Correct order for doc block * clean up with pint * expect annotations * test for web search in messages with url_citations * Just return annotations, no need for filling another array map * Handle ResponseChoiceAnnotations as object then use toArray method * Retun annotations from class as expected * Correct docblocks * check urlCitations in createResponseChoiceAnnotaitons * point to correct item in array for test * pint * Correct doc block with single quotes for url_citations * doc blocks with single quotes for url_citation * Remove array filter on array map for urlCitations * Assert toArray with function call * Annotations treated as arrays on message object * WIP Type fix with PHPstan * Fix doc blocks * fix doc block * test: augment tests for annotations --- src/Responses/Chat/CreateResponse.php | 6 +-- src/Responses/Chat/CreateResponseChoice.php | 4 +- .../Chat/CreateResponseChoiceAnnotations.php | 33 ++++++++++++++++ ...eResponseChoiceAnnotationsUrlCitations.php | 39 +++++++++++++++++++ src/Responses/Chat/CreateResponseMessage.php | 15 ++++++- tests/Fixtures/Chat.php | 34 ++++++++++++++++ .../Chat/CreateResponseChoiceAnnotations.php | 19 +++++++++ ...ponseChoiceAnnotationsUrlCitationsTest.php | 20 ++++++++++ .../Responses/Chat/CreateResponseMessage.php | 21 ++++++++++ 9 files changed, 184 insertions(+), 7 deletions(-) create mode 100644 src/Responses/Chat/CreateResponseChoiceAnnotations.php create mode 100644 src/Responses/Chat/CreateResponseChoiceAnnotationsUrlCitations.php create mode 100644 tests/Responses/Chat/CreateResponseChoiceAnnotations.php create mode 100644 tests/Responses/Chat/CreateResponseChoiceAnnotationsUrlCitationsTest.php diff --git a/src/Responses/Chat/CreateResponse.php b/src/Responses/Chat/CreateResponse.php index 88cb5c4a..b6492c15 100644 --- a/src/Responses/Chat/CreateResponse.php +++ b/src/Responses/Chat/CreateResponse.php @@ -12,12 +12,12 @@ 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, function_call?: array{name: string, arguments: string}, tool_calls?: array}, 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 { /** - * @use ArrayAccessible}, logprobs: ?array{content: ?array}>}, finish_reason: string|null}>, usage: array{prompt_tokens: int, completion_tokens: int|null, total_tokens: int}}> + * @use ArrayAccessible, function_call?: array{name: string, arguments: string}, tool_calls?: array}, logprobs: ?array{content: ?array}>}, finish_reason: string|null}>, usage: array{prompt_tokens: int, completion_tokens: int|null, total_tokens: int}}> */ use ArrayAccessible; @@ -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}, 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, function_call: ?array{name: string, arguments: string}, tool_calls: ?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 { diff --git a/src/Responses/Chat/CreateResponseChoice.php b/src/Responses/Chat/CreateResponseChoice.php index a592d9de..c5097e14 100644 --- a/src/Responses/Chat/CreateResponseChoice.php +++ b/src/Responses/Chat/CreateResponseChoice.php @@ -14,7 +14,7 @@ private function __construct( ) {} /** - * @param array{index: int, message: array{role: string, content: ?string, function_call: ?array{name: string, arguments: string}, tool_calls: ?array}, logprobs?: ?array{content: ?array}>}, finish_reason: string|null} $attributes + * @param array{index: int, message: array{role: string, content: ?string, annotations?: array, function_call: ?array{name: string, arguments: string}, tool_calls: ?array} ,logprobs?: ?array{content: ?array}>}, finish_reason: string|null} $attributes */ public static function from(array $attributes): self { @@ -27,7 +27,7 @@ public static function from(array $attributes): self } /** - * @return array{index: int, message: array{role: string, content: string|null, function_call?: array{name: string, arguments: string}, tool_calls?: array}, logprobs: ?array{content: ?array}>}, finish_reason: string|null} + * @return array{index: int, message: array{role: string, content: string|null, annotations?: array, function_call?: array{name: string, arguments: string}, tool_calls?: array}, logprobs: ?array{content: ?array}>}, finish_reason: string|null} */ public function toArray(): array { diff --git a/src/Responses/Chat/CreateResponseChoiceAnnotations.php b/src/Responses/Chat/CreateResponseChoiceAnnotations.php new file mode 100644 index 00000000..c0163f58 --- /dev/null +++ b/src/Responses/Chat/CreateResponseChoiceAnnotations.php @@ -0,0 +1,33 @@ + $this->type, + 'url_citation' => $this->urlCitations->toArray(), + ]; + } +} diff --git a/src/Responses/Chat/CreateResponseChoiceAnnotationsUrlCitations.php b/src/Responses/Chat/CreateResponseChoiceAnnotationsUrlCitations.php new file mode 100644 index 00000000..a640f337 --- /dev/null +++ b/src/Responses/Chat/CreateResponseChoiceAnnotationsUrlCitations.php @@ -0,0 +1,39 @@ + $this->endIndex, + 'start_index' => $this->startIndex, + 'title' => $this->title, + 'url' => $this->url, + ]; + } +} diff --git a/src/Responses/Chat/CreateResponseMessage.php b/src/Responses/Chat/CreateResponseMessage.php index 48ae6293..4ae4699a 100644 --- a/src/Responses/Chat/CreateResponseMessage.php +++ b/src/Responses/Chat/CreateResponseMessage.php @@ -8,16 +8,18 @@ final class CreateResponseMessage { /** * @param array $toolCalls + * @param array $annotations */ private function __construct( public readonly string $role, public readonly ?string $content, + public readonly array $annotations, public readonly array $toolCalls, public readonly ?CreateResponseFunctionCall $functionCall, ) {} /** - * @param array{role: string, content: ?string, function_call: ?array{name: string, arguments: string}, tool_calls: ?array} $attributes + * @param array{role: string, content: ?string, annotations?: array, function_call: ?array{name: string, arguments: string}, tool_calls: ?array} $attributes */ public static function from(array $attributes): self { @@ -25,16 +27,21 @@ public static function from(array $attributes): self $result ), $attributes['tool_calls'] ?? []); + $annotations = array_map(fn (array $result): CreateResponseChoiceAnnotations => CreateResponseChoiceAnnotations::from( + $result, + ), $attributes['annotations'] ?? []); + return new self( $attributes['role'], $attributes['content'] ?? null, + $annotations, $toolCalls, isset($attributes['function_call']) ? CreateResponseFunctionCall::from($attributes['function_call']) : null, ); } /** - * @return array{role: string, content: string|null, function_call?: array{name: string, arguments: string}, tool_calls?: array} + * @return array{role: string, content: string|null, annotations?: array, function_call?: array{name: string, arguments: string}, tool_calls?: array} */ public function toArray(): array { @@ -43,6 +50,10 @@ public function toArray(): array 'content' => $this->content, ]; + if ($this->annotations !== []) { + $data['annotations'] = array_map(fn (CreateResponseChoiceAnnotations $annotations): array => $annotations->toArray(), $this->annotations); + } + if ($this->functionCall instanceof CreateResponseFunctionCall) { $data['function_call'] = $this->functionCall->toArray(); } diff --git a/tests/Fixtures/Chat.php b/tests/Fixtures/Chat.php index a30996a5..94aed2ca 100644 --- a/tests/Fixtures/Chat.php +++ b/tests/Fixtures/Chat.php @@ -237,6 +237,40 @@ function chatCompletionWithoutUsage(): array ]; } +/** + * @return array + */ +function chatCompletionWithAnnotations(): array +{ + return [ + 'id' => 'chatcmpl-123', + 'object' => 'chat.completion', + 'created' => 1677652288, + 'model' => 'gpt-4o-mini-search-preview', + 'choices' => [ + [ + 'index' => 0, + 'message' => [ + 'role' => 'assistant', + 'content' => 'Hello World', + 'annotations' => [ + [ + 'type' => 'url_citation', + 'url_citation' => [ + 'end_index' => 5, + 'start_index' => 0, + 'title' => 'Hello', + 'url' => 'https://example.com', + ], + ], + ], + ], + 'finish_reason' => 'stop', + ], + ], + ]; +} + /** * @return array */ diff --git a/tests/Responses/Chat/CreateResponseChoiceAnnotations.php b/tests/Responses/Chat/CreateResponseChoiceAnnotations.php new file mode 100644 index 00000000..9940b228 --- /dev/null +++ b/tests/Responses/Chat/CreateResponseChoiceAnnotations.php @@ -0,0 +1,19 @@ +type->toBe('url_citation') + ->urlCitations->toBeInstanceOf(CreateResponseChoiceAnnotationsUrlCitations::class); +}); + +test('to array', function () { + $result = CreateResponseChoiceAnnotations::from(chatCompletionWithAnnotations()['choices'][0]['message']['annotations'][0]); + + expect($result->toArray()) + ->toBe(chatCompletionWithAnnotations()['choices'][0]['message']['annotations'][0]); +}); diff --git a/tests/Responses/Chat/CreateResponseChoiceAnnotationsUrlCitationsTest.php b/tests/Responses/Chat/CreateResponseChoiceAnnotationsUrlCitationsTest.php new file mode 100644 index 00000000..a177f75d --- /dev/null +++ b/tests/Responses/Chat/CreateResponseChoiceAnnotationsUrlCitationsTest.php @@ -0,0 +1,20 @@ +endIndex->toBe(5) + ->startIndex->toBe(0) + ->title->toBe('Hello') + ->url->toBe('https://example.com'); +}); + +test('to array', function () { + $result = CreateResponseChoiceAnnotationsUrlCitations::from(chatCompletionWithAnnotations()['choices'][0]['message']['annotations'][0]['url_citation']); + + expect($result->toArray()) + ->toBe(chatCompletionWithAnnotations()['choices'][0]['message']['annotations'][0]['url_citation']); +}); diff --git a/tests/Responses/Chat/CreateResponseMessage.php b/tests/Responses/Chat/CreateResponseMessage.php index 452866b6..79176d73 100644 --- a/tests/Responses/Chat/CreateResponseMessage.php +++ b/tests/Responses/Chat/CreateResponseMessage.php @@ -1,5 +1,6 @@ role->toBe('assistant') ->content->toBe("\n\nHello there, how may I assist you today?") + ->annotations->toBeArray() ->functionCall->toBeNull(); }); @@ -19,6 +21,7 @@ expect($result) ->role->toBe('assistant') ->content->toBeNull() + ->annotations->toBeArray() ->functionCall->toBeInstanceOf(CreateResponseFunctionCall::class); }); @@ -33,6 +36,17 @@ ->toolCalls->each->toBeInstanceOf(CreateResponseToolCall::class); }); +test('from annotations response', function () { + $result = CreateResponseMessage::from(chatCompletionWithAnnotations()['choices'][0]['message']); + + expect($result) + ->role->toBe('assistant') + ->content->toBe('Hello World') + ->annotations->toBeArray() + ->annotations->toHaveCount(1) + ->annotations->each->toBeInstanceOf(CreateResponseChoiceAnnotations::class); +}); + test('from function response without content', function () { $result = CreateResponseMessage::from(chatCompletionMessageWithFunctionAndNoContent()); @@ -62,3 +76,10 @@ expect($result->toArray()) ->toBe(chatCompletionWithToolCalls()['choices'][0]['message']); }); + +test('to array from annotations response', function () { + $result = CreateResponseMessage::from(chatCompletionWithAnnotations()['choices'][0]['message']); + + expect($result->toArray()) + ->toBe(chatCompletionWithAnnotations()['choices'][0]['message']); +}); From 74b7851254feec79331560702fae0b05e5fb8d70 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 4 May 2025 14:22:51 +0100 Subject: [PATCH 26/29] chore: bumps dependencies --- composer.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 77194624..34afffab 100644 --- a/composer.json +++ b/composer.json @@ -22,16 +22,16 @@ "psr/http-message": "^1.1.0|^2.0.0" }, "require-dev": { - "guzzlehttp/guzzle": "^7.9.2", - "guzzlehttp/psr7": "^2.7.0", - "laravel/pint": "^1.18.1", + "guzzlehttp/guzzle": "^7.9.3", + "guzzlehttp/psr7": "^2.7.1", + "laravel/pint": "^1.22.0", "mockery/mockery": "^1.6.12", - "nunomaduro/collision": "^7.11.0|^8.5.0", - "pestphp/pest": "^2.36.0|^3.5.0", - "pestphp/pest-plugin-arch": "^2.7|^3.0", - "pestphp/pest-plugin-type-coverage": "^2.8.7|^3.1.0", - "phpstan/phpstan": "^1.12.7", - "symfony/var-dumper": "^6.4.11|^7.1.5" + "nunomaduro/collision": "^7.11.0|^8.8.0", + "pestphp/pest": "^2.36.0|^3.8.2", + "pestphp/pest-plugin-arch": "^2.7|^3.1.1", + "pestphp/pest-plugin-type-coverage": "^2.8.7|^3.5.1", + "phpstan/phpstan": "^1.12.25", + "symfony/var-dumper": "^6.4.11|^7.2.6" }, "autoload": { "psr-4": { From 31b4dcc9b90bd8a82cfd9df545832a07ac35b764 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 4 May 2025 14:28:52 +0100 Subject: [PATCH 27/29] chore: install Pest 4 --- .github/workflows/tests.yml | 6 +++++- composer.json | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d2fa10af..e02d05a2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,9 +10,10 @@ jobs: matrix: os: [ubuntu-latest] php: [8.1, 8.2, 8.3, 8.4] + pest: [2, 3, 4] dependency-version: [prefer-lowest, prefer-stable] - name: Tests P${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }} + name: Tests P${{ matrix.php }} - Pest ${{ matrix.pest }} - ${{ matrix.os }} - ${{ matrix.dependency-version }} steps: @@ -32,6 +33,9 @@ jobs: extensions: dom, mbstring, zip coverage: none + - name: Install Pest + run: composer require pestphp/pest:^${{ matrix.pest }} --dev --no-update --with-all-dependencies + - name: Install Composer dependencies run: composer update --${{ matrix.dependency-version }} --no-interaction --prefer-dist diff --git a/composer.json b/composer.json index 34afffab..539cba20 100644 --- a/composer.json +++ b/composer.json @@ -27,9 +27,9 @@ "laravel/pint": "^1.22.0", "mockery/mockery": "^1.6.12", "nunomaduro/collision": "^7.11.0|^8.8.0", - "pestphp/pest": "^2.36.0|^3.8.2", - "pestphp/pest-plugin-arch": "^2.7|^3.1.1", - "pestphp/pest-plugin-type-coverage": "^2.8.7|^3.5.1", + "pestphp/pest": "^2.36.0|^3.8.2|^4.0.0", + "pestphp/pest-plugin-arch": "^2.7|^3.1.1|^4.0.0", + "pestphp/pest-plugin-type-coverage": "^2.8.7|^3.5.1|^4.0.0", "phpstan/phpstan": "^1.12.25", "symfony/var-dumper": "^6.4.11|^7.2.6" }, From d6be4188abce8e07722d81262f80e20ebd0070df Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 4 May 2025 14:32:00 +0100 Subject: [PATCH 28/29] chore: drops PHP 8.1 --- .github/workflows/formats.yml | 2 +- .github/workflows/tests.yml | 4 ++-- composer.json | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/formats.yml b/.github/workflows/formats.yml index f3b06a0c..e77f8e9a 100644 --- a/.github/workflows/formats.yml +++ b/.github/workflows/formats.yml @@ -10,7 +10,7 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest] - php: [8.1] + php: [8.2] dependency-version: [prefer-lowest, prefer-stable] name: Formats P${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e02d05a2..4d2ccea1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,8 +9,8 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest] - php: [8.1, 8.2, 8.3, 8.4] - pest: [2, 3, 4] + php: [8.2, 8.3, 8.4] + pest: [3, 4] dependency-version: [prefer-lowest, prefer-stable] name: Tests P${{ matrix.php }} - Pest ${{ matrix.pest }} - ${{ matrix.os }} - ${{ matrix.dependency-version }} diff --git a/composer.json b/composer.json index 539cba20..b5747463 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ } ], "require": { - "php": "^8.1.0", + "php": "^8.2.0", "php-http/discovery": "^1.20.0", "php-http/multipart-stream-builder": "^1.4.2", "psr/http-client": "^1.0.3", @@ -26,12 +26,12 @@ "guzzlehttp/psr7": "^2.7.1", "laravel/pint": "^1.22.0", "mockery/mockery": "^1.6.12", - "nunomaduro/collision": "^7.11.0|^8.8.0", - "pestphp/pest": "^2.36.0|^3.8.2|^4.0.0", - "pestphp/pest-plugin-arch": "^2.7|^3.1.1|^4.0.0", - "pestphp/pest-plugin-type-coverage": "^2.8.7|^3.5.1|^4.0.0", + "nunomaduro/collision": "^8.8.0", + "pestphp/pest": "^3.8.2|^4.0.0", + "pestphp/pest-plugin-arch": "^3.1.1|^4.0.0", + "pestphp/pest-plugin-type-coverage": "^3.5.1|^4.0.0", "phpstan/phpstan": "^1.12.25", - "symfony/var-dumper": "^6.4.11|^7.2.6" + "symfony/var-dumper": "^7.2.6" }, "autoload": { "psr-4": { From 87cb05309dc1b3ba96725dfeb66186df83a2d489 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Sun, 4 May 2025 14:34:48 +0100 Subject: [PATCH 29/29] chore: excludes 8.2 on pest 4 --- .github/workflows/tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4d2ccea1..05ce8cce 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,6 +12,9 @@ jobs: php: [8.2, 8.3, 8.4] pest: [3, 4] dependency-version: [prefer-lowest, prefer-stable] + exclude: + - php: 8.2 + pest: 4 name: Tests P${{ matrix.php }} - Pest ${{ matrix.pest }} - ${{ matrix.os }} - ${{ matrix.dependency-version }}