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