From 3356a26b23e3e6b0595b07b7ecfcc4e26b56d1ef Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Mon, 5 May 2025 00:21:32 +0100 Subject: [PATCH 01/14] docs: Corrected readme for current PHP version requirement (#575) * Corrected readme for current PHP version requirement * Update link to canonical version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e59185f4..59ad3882 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ If you or your business relies on this package, it's important to support the de ## Get Started -> **Requires [PHP 8.1+](https://php.net/releases/)** +> **Requires [PHP 8.2+](https://www.php.net/releases/)** First, install OpenAI via the [Composer](https://getcomposer.org/) package manager: From 2241ae0c69aa02425fb0d4bf25f421d1ce5a56f2 Mon Sep 17 00:00:00 2001 From: Adrian M Date: Wed, 7 May 2025 17:04:13 +0200 Subject: [PATCH 02/14] fix(test): Add Throwable type to ClientFake response annotations (#576) * Add Throwable type to ClientFake response annotations * Remove redundant "composer refactor" command from CONTRIBUTING.md * Add missing status code in the README.md Testing section --- CONTRIBUTING.md | 12 ------------ README.md | 2 +- src/Testing/ClientFake.php | 4 ++-- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9f17d605..2b9d58bb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,13 +24,6 @@ Clone your fork, then install the dev dependencies: composer install ``` -## Refactor - -Refactor your code: -```bash -composer refactor -``` - ## Lint Lint your code: @@ -45,11 +38,6 @@ Run all tests: composer test ``` -Check code quality: -```bash -composer test:refactor -``` - Check types: ```bash composer test:types diff --git a/README.md b/README.md index 59ad3882..9a27bf36 100644 --- a/README.md +++ b/README.md @@ -2419,7 +2419,7 @@ $client = new ClientFake([ 'message' => 'The model `gpt-1` does not exist', 'type' => 'invalid_request_error', 'code' => null, - ]) + ], 404) ]); // the `ErrorException` will be thrown diff --git a/src/Testing/ClientFake.php b/src/Testing/ClientFake.php index 828f2a28..428c5b33 100644 --- a/src/Testing/ClientFake.php +++ b/src/Testing/ClientFake.php @@ -34,12 +34,12 @@ class ClientFake implements ClientContract private array $requests = []; /** - * @param array $responses + * @param array $responses */ public function __construct(protected array $responses = []) {} /** - * @param array $responses + * @param array $responses */ public function addResponses(array $responses): void { From 27f932909893e341634e97cd6e89138eebab1832 Mon Sep 17 00:00:00 2001 From: momostafa Date: Wed, 14 May 2025 23:02:16 +0200 Subject: [PATCH 03/14] Add support for Responses API (#541) * Add support for Responses API Full support to the new Responses API https://platform.openai.com/docs/api-reference/responses * Updated miss placed files that occurred during first upload * Heavy refactoring to match codebase pattern, added testing files * fixed some phpstan errors * fixing minor bugs after live testing now all models work during live testing * Fixed lint errors * Fixed all PHPStan Errors and all other tests Pass 100% * Completed missing Tests, Created ClientFakeResponses, Modified Fakeable I had to modify OpenAI\Testing\Responses\Concerns\Fakeable as $class = str_replace('Responses\\', 'Testing\\Responses\\Fixtures\\', static::class).'Fixture'; was conflicting with newly added Responses folder and added docblock explaining the modification and tested against all files. Updated readme can be found at README-RESPONSES.md Added dedicated ClientFake for Responses tests/Testing/ClientFakeResponses.php * Updated Test files, Fixed Lint errors, all tests pass except test:unit * chore: remove log file * chore: pint * chore: inline Responses doc to readme * chore: align client contract to pattern * docs: re-order chat/completion * fix: use longer replacement to not clobber Responses/* * fix: parse metadata (optionally) * test: don't assert on plain arrays * test: assert stream properly on responses * chore: add missing docblock property * fix: add metadata into Responses payload * test: fix double nesting on delete attrs * test: correct bad assertions on tests * fix: remove ResponseObject * feat: split out usage into classes * feat: split out error into classes * feat: split out incomplete_details into classes * chore(wip): introduction of 'output' typing * fix: correct OutputMessageContentOutputText + child classes * OutputMessage * chore: continued work on output message * chore: start of ComputerToolActions * feat: complete "computer tool call" * fix: add response contract to existing classes * fix: more fixes to typing on CreateResponse * feat: add 'reasoning' prop for response * feat: add 'text' (format) to create * feat: add 'tool_choice' * fix: add 'truncation' * feat: add tool: FileSearch response * feat: add tool: FunctionTool response * feat: add tool 'ComputerUse' * fix: wire up 'tools' to CreateResponse * Added InputMessage types, refactored ListInputItems - Added Classes related to InputMessage types - refactored ListInputItems to reflect the newly added classes * Corrected some properties to use camelCase * fix: cleanup docblock on CreateResponse * chore: remove unused test response object * chore: remove extra newline * fix: 'role' is always 'assistant' * test: augment tests with single click * chore: add missing int for computer single click * test: progression towards 100% coverage on create response * test: assertion on OutputFileSearchToolCall * chore: ints for x/y on click event * fix: further cleanup on CreateResponse * feat: add computer tool call output * feat: add function tool call output * fix: city, region and timezone can return null * fix: json_schema description can be missing * Lint Test Pass, Fixed Array map at ListInputItems * chore: rework text format typing on create response * feat: rework into phpstan-types * fix: work on ListInputItems for phpstan-types * fix: wire up RetrieveResponse * fix: migrate to phpstan-type for delete * fix: wip towards stream responses * chore: add typing props for delete * test: correct tests for retrieve response * fix: add outputItem types for streaming * fix: contentPart/textDelta/textAnnotation added * fix: outputrefusal*/outputText wired up * chore: rename Refusal types * feat: finish typing for streaming * Wired all classes under Responses/Input * chore: type ListInputItems * fix: finalize ListInputItems * test: ListInputItem tests * test: stream testing * fix: shove mock meta object into stream * test: correct streaming tests * chore: use local type * docs: cleanup readme * chore: align constructors * fix: mark 'status' as optional in reasoning * fix: add encrypted_content to reasoning * fix: file search filters can be null --------- Co-authored-by: Connor Tumbleson Co-authored-by: Connor Tumbleson --- README.md | 194 ++++++++++ src/Client.php | 11 + src/Contracts/ClientContract.php | 8 + src/Contracts/Resources/ResponsesContract.php | 60 +++ src/Resources/Responses.php | 114 ++++++ src/Responses/Responses/CreateResponse.php | 208 +++++++++++ .../Responses/CreateResponseError.php | 51 +++ .../Responses/CreateResponseFormat.php | 61 +++ .../CreateResponseIncompleteDetails.php | 48 +++ .../Responses/CreateResponseReasoning.php | 51 +++ .../Responses/CreateResponseUsage.php | 63 ++++ .../CreateResponseUsageInputTokenDetails.php | 48 +++ .../CreateResponseUsageOutputTokenDetails.php | 48 +++ .../Responses/CreateStreamedResponse.php | 102 +++++ src/Responses/Responses/DeleteResponse.php | 60 +++ .../Responses/Format/JsonObjectFormat.php | 51 +++ .../Responses/Format/JsonSchemaFormat.php | 64 ++++ src/Responses/Responses/Format/TextFormat.php | 51 +++ .../Input/AcknowledgedSafetyCheck.php | 54 +++ .../Input/ComputerToolCallOutput.php | 79 ++++ .../ComputerToolCallOutputScreenshot.php | 57 +++ .../Input/FunctionToolCallOutput.php | 64 ++++ .../Responses/Input/InputMessage.php | 82 ++++ .../Input/InputMessageContentInputFile.php | 60 +++ .../Input/InputMessageContentInputImage.php | 60 +++ .../Input/InputMessageContentInputText.php | 54 +++ src/Responses/Responses/ListInputItems.php | 101 +++++ .../OutputComputerActionClick.php | 61 +++ .../OutputComputerActionDoubleClick.php | 57 +++ .../OutputComputerActionDrag.php | 65 ++++ .../OutputComputerActionKeyPress.php | 55 +++ .../OutputComputerActionMove.php | 57 +++ .../OutputComputerActionScreenshot.php | 51 +++ .../OutputComputerActionScroll.php | 63 ++++ .../OutputComputerActionType.php | 54 +++ .../OutputComputerActionWait.php | 51 +++ .../ComputerAction/OutputComputerDragPath.php | 51 +++ .../OutputComputerPendingSafetyCheck.php | 54 +++ .../Output/OutputComputerToolCall.php | 109 ++++++ .../Output/OutputFileSearchToolCall.php | 78 ++++ .../Output/OutputFileSearchToolCallResult.php | 63 ++++ .../Output/OutputFunctionToolCall.php | 67 ++++ .../Responses/Output/OutputMessage.php | 80 ++++ .../Output/OutputMessageContentOutputText.php | 77 ++++ ...ntentOutputTextAnnotationsFileCitation.php | 57 +++ ...geContentOutputTextAnnotationsFilePath.php | 57 +++ ...ontentOutputTextAnnotationsUrlCitation.php | 63 ++++ .../Output/OutputMessageContentRefusal.php | 54 +++ .../Responses/Output/OutputReasoning.php | 75 ++++ .../Output/OutputReasoningSummary.php | 54 +++ .../Output/OutputWebSearchToolCall.php | 57 +++ src/Responses/Responses/RetrieveResponse.php | 208 +++++++++++ .../Responses/Streaming/ContentPart.php | 73 ++++ src/Responses/Responses/Streaming/Error.php | 60 +++ .../Responses/Streaming/FileSearchCall.php | 57 +++ .../Streaming/FunctionCallArgumentsDelta.php | 60 +++ .../Streaming/FunctionCallArgumentsDone.php | 60 +++ .../Responses/Streaming/OutputItem.php | 79 ++++ .../Streaming/OutputTextAnnotationAdded.php | 79 ++++ .../Responses/Streaming/OutputTextDelta.php | 63 ++++ .../Responses/Streaming/OutputTextDone.php | 63 ++++ .../Streaming/ReasoningSummaryPart.php | 66 ++++ .../Streaming/ReasoningSummaryTextDelta.php | 63 ++++ .../Streaming/ReasoningSummaryTextDone.php | 63 ++++ .../Responses/Streaming/RefusalDelta.php | 63 ++++ .../Responses/Streaming/RefusalDone.php | 63 ++++ .../Responses/Streaming/WebSearchCall.php | 57 +++ .../Responses/Tool/ComputerUseTool.php | 60 +++ .../Tool/FileSearchComparisonFilter.php | 57 +++ .../Tool/FileSearchCompoundFilter.php | 65 ++++ .../Tool/FileSearchRankingOption.php | 51 +++ .../Responses/Tool/FileSearchTool.php | 77 ++++ src/Responses/Responses/Tool/FunctionTool.php | 64 ++++ .../Responses/Tool/WebSearchTool.php | 62 ++++ .../Responses/Tool/WebSearchUserLocation.php | 63 ++++ .../ToolChoice/FunctionToolChoice.php | 54 +++ .../Responses/ToolChoice/HostedToolChoice.php | 51 +++ src/Responses/StreamResponse.php | 2 +- src/Testing/ClientFake.php | 6 + .../Resources/ResponsesTestResource.php | 47 +++ src/Testing/Responses/Concerns/Fakeable.php | 2 +- .../Responses/CreateResponseFixture.php | 103 ++++++ .../CreateStreamedResponseFixture.txt | 9 + .../Responses/DeleteResponseFixture.php | 12 + .../Responses/ListInputItemsFixture.php | 28 ++ .../Responses/ResponseObjectFixture.php | 103 ++++++ .../Responses/RetrieveResponseFixture.php | 103 ++++++ tests/Fixtures/Responses.php | 349 ++++++++++++++++++ .../Streams/ResponseCompletionCreate.txt | 11 + .../Streams/ResponseCreatedResponse.txt | 1 + tests/Resources/Responses.php | 203 ++++++++++ tests/Responses/Responses/CreateResponse.php | 74 ++++ .../Responses/CreateStreamedResponse.php | 22 ++ tests/Responses/Responses/DeleteResponse.php | 47 +++ tests/Responses/Responses/ListInputItems.php | 53 +++ .../Output/OutputComputerToolCall.php | 30 ++ .../Output/OutputFileSearchToolCall.php | 42 +++ .../Responses/Output/OutputReasoning.php | 44 +++ .../Responses/Responses/RetrieveResponse.php | 77 ++++ .../Responses/Tool/FileSearchTool.php | 54 +++ tests/Testing/ClientFakeResponses.php | 158 ++++++++ .../Resources/ResponsesTestResource.php | 74 ++++ 102 files changed, 6847 insertions(+), 2 deletions(-) create mode 100644 src/Contracts/Resources/ResponsesContract.php create mode 100644 src/Resources/Responses.php create mode 100644 src/Responses/Responses/CreateResponse.php create mode 100644 src/Responses/Responses/CreateResponseError.php create mode 100644 src/Responses/Responses/CreateResponseFormat.php create mode 100644 src/Responses/Responses/CreateResponseIncompleteDetails.php create mode 100644 src/Responses/Responses/CreateResponseReasoning.php create mode 100644 src/Responses/Responses/CreateResponseUsage.php create mode 100644 src/Responses/Responses/CreateResponseUsageInputTokenDetails.php create mode 100644 src/Responses/Responses/CreateResponseUsageOutputTokenDetails.php create mode 100644 src/Responses/Responses/CreateStreamedResponse.php create mode 100644 src/Responses/Responses/DeleteResponse.php create mode 100644 src/Responses/Responses/Format/JsonObjectFormat.php create mode 100644 src/Responses/Responses/Format/JsonSchemaFormat.php create mode 100644 src/Responses/Responses/Format/TextFormat.php create mode 100644 src/Responses/Responses/Input/AcknowledgedSafetyCheck.php create mode 100644 src/Responses/Responses/Input/ComputerToolCallOutput.php create mode 100644 src/Responses/Responses/Input/ComputerToolCallOutputScreenshot.php create mode 100644 src/Responses/Responses/Input/FunctionToolCallOutput.php create mode 100644 src/Responses/Responses/Input/InputMessage.php create mode 100644 src/Responses/Responses/Input/InputMessageContentInputFile.php create mode 100644 src/Responses/Responses/Input/InputMessageContentInputImage.php create mode 100644 src/Responses/Responses/Input/InputMessageContentInputText.php create mode 100644 src/Responses/Responses/ListInputItems.php create mode 100644 src/Responses/Responses/Output/ComputerAction/OutputComputerActionClick.php create mode 100644 src/Responses/Responses/Output/ComputerAction/OutputComputerActionDoubleClick.php create mode 100644 src/Responses/Responses/Output/ComputerAction/OutputComputerActionDrag.php create mode 100644 src/Responses/Responses/Output/ComputerAction/OutputComputerActionKeyPress.php create mode 100644 src/Responses/Responses/Output/ComputerAction/OutputComputerActionMove.php create mode 100644 src/Responses/Responses/Output/ComputerAction/OutputComputerActionScreenshot.php create mode 100644 src/Responses/Responses/Output/ComputerAction/OutputComputerActionScroll.php create mode 100644 src/Responses/Responses/Output/ComputerAction/OutputComputerActionType.php create mode 100644 src/Responses/Responses/Output/ComputerAction/OutputComputerActionWait.php create mode 100644 src/Responses/Responses/Output/ComputerAction/OutputComputerDragPath.php create mode 100644 src/Responses/Responses/Output/ComputerAction/OutputComputerPendingSafetyCheck.php create mode 100644 src/Responses/Responses/Output/OutputComputerToolCall.php create mode 100644 src/Responses/Responses/Output/OutputFileSearchToolCall.php create mode 100644 src/Responses/Responses/Output/OutputFileSearchToolCallResult.php create mode 100644 src/Responses/Responses/Output/OutputFunctionToolCall.php create mode 100644 src/Responses/Responses/Output/OutputMessage.php create mode 100644 src/Responses/Responses/Output/OutputMessageContentOutputText.php create mode 100644 src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsFileCitation.php create mode 100644 src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsFilePath.php create mode 100644 src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsUrlCitation.php create mode 100644 src/Responses/Responses/Output/OutputMessageContentRefusal.php create mode 100644 src/Responses/Responses/Output/OutputReasoning.php create mode 100644 src/Responses/Responses/Output/OutputReasoningSummary.php create mode 100644 src/Responses/Responses/Output/OutputWebSearchToolCall.php create mode 100644 src/Responses/Responses/RetrieveResponse.php create mode 100644 src/Responses/Responses/Streaming/ContentPart.php create mode 100644 src/Responses/Responses/Streaming/Error.php create mode 100644 src/Responses/Responses/Streaming/FileSearchCall.php create mode 100644 src/Responses/Responses/Streaming/FunctionCallArgumentsDelta.php create mode 100644 src/Responses/Responses/Streaming/FunctionCallArgumentsDone.php create mode 100644 src/Responses/Responses/Streaming/OutputItem.php create mode 100644 src/Responses/Responses/Streaming/OutputTextAnnotationAdded.php create mode 100644 src/Responses/Responses/Streaming/OutputTextDelta.php create mode 100644 src/Responses/Responses/Streaming/OutputTextDone.php create mode 100644 src/Responses/Responses/Streaming/ReasoningSummaryPart.php create mode 100644 src/Responses/Responses/Streaming/ReasoningSummaryTextDelta.php create mode 100644 src/Responses/Responses/Streaming/ReasoningSummaryTextDone.php create mode 100644 src/Responses/Responses/Streaming/RefusalDelta.php create mode 100644 src/Responses/Responses/Streaming/RefusalDone.php create mode 100644 src/Responses/Responses/Streaming/WebSearchCall.php create mode 100644 src/Responses/Responses/Tool/ComputerUseTool.php create mode 100644 src/Responses/Responses/Tool/FileSearchComparisonFilter.php create mode 100644 src/Responses/Responses/Tool/FileSearchCompoundFilter.php create mode 100644 src/Responses/Responses/Tool/FileSearchRankingOption.php create mode 100644 src/Responses/Responses/Tool/FileSearchTool.php create mode 100644 src/Responses/Responses/Tool/FunctionTool.php create mode 100644 src/Responses/Responses/Tool/WebSearchTool.php create mode 100644 src/Responses/Responses/Tool/WebSearchUserLocation.php create mode 100644 src/Responses/Responses/ToolChoice/FunctionToolChoice.php create mode 100644 src/Responses/Responses/ToolChoice/HostedToolChoice.php create mode 100644 src/Testing/Resources/ResponsesTestResource.php create mode 100644 src/Testing/Responses/Fixtures/Responses/CreateResponseFixture.php create mode 100644 src/Testing/Responses/Fixtures/Responses/CreateStreamedResponseFixture.txt create mode 100644 src/Testing/Responses/Fixtures/Responses/DeleteResponseFixture.php create mode 100644 src/Testing/Responses/Fixtures/Responses/ListInputItemsFixture.php create mode 100644 src/Testing/Responses/Fixtures/Responses/ResponseObjectFixture.php create mode 100644 src/Testing/Responses/Fixtures/Responses/RetrieveResponseFixture.php create mode 100644 tests/Fixtures/Responses.php create mode 100644 tests/Fixtures/Streams/ResponseCompletionCreate.txt create mode 100644 tests/Fixtures/Streams/ResponseCreatedResponse.txt create mode 100644 tests/Resources/Responses.php create mode 100644 tests/Responses/Responses/CreateResponse.php create mode 100644 tests/Responses/Responses/CreateStreamedResponse.php create mode 100644 tests/Responses/Responses/DeleteResponse.php create mode 100644 tests/Responses/Responses/ListInputItems.php create mode 100644 tests/Responses/Responses/Output/OutputComputerToolCall.php create mode 100644 tests/Responses/Responses/Output/OutputFileSearchToolCall.php create mode 100644 tests/Responses/Responses/Output/OutputReasoning.php create mode 100644 tests/Responses/Responses/RetrieveResponse.php create mode 100644 tests/Responses/Responses/Tool/FileSearchTool.php create mode 100644 tests/Testing/ClientFakeResponses.php create mode 100644 tests/Testing/Resources/ResponsesTestResource.php diff --git a/README.md b/README.md index 9a27bf36..25fd7467 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ 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) + - [Responses Resource](#responses-resource) - [Chat Resource](#chat-resource) - [Completions Resource](#completions-resource) - [Audio Resource](#audio-resource) @@ -154,6 +155,199 @@ $response->deleted; // true $response->toArray(); // ['id' => 'curie:ft-acmeco-2021-03-03-21-44-20', ...] ``` +### `Responses` Resource + +#### `create` + +Creates a model response. Provide text or image inputs to generate text or JSON outputs. Have the model call your own custom code or use built-in tools like web search or file search to use your own data as input for the model's response. + +```php +$response = $client->responses()->create([ + 'model' => 'gpt-4o-mini', + 'tools' => [ + [ + 'type' => 'web_search_preview' + ] + ], + 'input' => "what was a positive news story from today?", + 'temperature' => 0.7, + 'max_output_tokens' => 150, + 'tool_choice' => 'auto', + 'parallel_tool_calls' => true, + 'store' => true, + 'metadata' => [ + 'user_id' => '123', + 'session_id' => 'abc456' + ] +]); + +$response->id; // 'resp_67ccd2bed1ec8190b14f964abc054267' +$response->object; // 'response' +$response->createdAt; // 1741476542 +$response->status; // 'completed' +$response->model; // 'gpt-4o-mini' + +foreach ($response->output as $output) { + $output->type; // 'message' + $output->id; // 'msg_67ccd2bf17f0819081ff3bb2cf6508e6' + $output->status; // 'completed' + $output->role; // 'assistant' + + foreach ($output->content as $content) { + $content->type; // 'output_text' + $content->text; // The response text + $content->annotations; // Any annotations in the response + } +} + +$response->usage->inputTokens; // 36 +$response->usage->outputTokens; // 87 +$response->usage->totalTokens; // 123 + +$response->toArray(); // ['id' => 'resp_67ccd2bed1ec8190b14f964abc054267', ...] +``` + +#### `create streamed` + +When you create a Response with stream set to true, the server will emit server-sent events to the client as the Response is generated. All events and their payloads can be found in [OpenAI docs](https://platform.openai.com/docs/api-reference/responses-streaming). + +```php +$stream = $client->responses()->createStreamed([ + 'model' => 'gpt-4o-mini', + 'tools' => [ + [ + 'type' => 'web_search_preview' + ] + ], + 'input' => "what was a positive news story from today?", +]); + +foreach ($stream as $response) { + $response->event; // 'response.created' +} +``` + +### `retrieve` + +Retrieves a model response with the given ID. + +```php +$response = $client->responses()->retrieve('resp_67ccd2bed1ec8190b14f964abc054267'); + +$response->id; // 'resp_67ccd2bed1ec8190b14f964abc054267' +$response->object; // 'response' +$response->createdAt; // 1741476542 +$response->status; // 'completed' +$response->error; // null +$response->incompleteDetails; // null +$response->instructions; // null +$response->maxOutputTokens; // null +$response->model; // 'gpt-4o-mini-2024-07-18"' +$response->parallelToolCalls; // true +$response->previousResponseId; // null +$response->store; // true +$response->temperature; // 1.0 +$response->toolChoice; // 'auto' +$response->topP; // 1.0 +$response->truncation; // 'disabled' + +$response->toArray(); // ['id' => 'resp_67ccd2bed1ec8190b14f964abc054267', ...] +``` + +### `delete` + +Deletes a model response with the given ID. + +```php +$response = $client->responses()->delete('resp_67ccd2bed1ec8190b14f964abc054267'); + +$response->id; // 'resp_67ccd2bed1ec8190b14f964abc054267' +$response->object; // 'response' +$response->deleted; // true + +$response->toArray(); // ['id' => 'resp_67ccd2bed1ec8190b14f964abc054267', 'deleted' => true, ...] +``` + +### `list` + +Lists input items for a response with the given ID. All events and their payloads can be found in [OpenAI docs](https://platform.openai.com/docs/api-reference/responses/list). + +```php +$response = $client->responses()->list('resp_67ccd2bed1ec8190b14f964abc054267', [ + 'limit' => 10, + 'order' => 'desc' +]); + +$response->object; // 'list' + +foreach ($response->data as $item) { + $item->type; // 'message' + $item->id; // 'msg_680bf4e8c1948192b64abf0bad54b30806e0834f49400fc3' + $item->status; // 'completed' + $item->role; // 'user' +} + +$response->firstId; // 'msg_680bf4e8c1948192b64abf0bad54b30806e0834f49400fc3' +$response->lastId; // 'msg_680bf4e8c1948192b64abf0bad54b30806e0834f49400fc3' +$response->hasMore; // false + +$response->toArray(); // ['object' => 'list', 'data' => [...], ...] +``` + +### `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` diff --git a/src/Client.php b/src/Client.php index c2547e13..3aca764f 100644 --- a/src/Client.php +++ b/src/Client.php @@ -21,6 +21,7 @@ use OpenAI\Resources\Images; use OpenAI\Resources\Models; use OpenAI\Resources\Moderations; +use OpenAI\Resources\Responses; use OpenAI\Resources\Threads; use OpenAI\Resources\VectorStores; @@ -34,6 +35,16 @@ public function __construct(private readonly TransporterContract $transporter) // .. } + /** + * Manage responses to assist models with tasks. + * + * @see https://platform.openai.com/docs/api-reference/responses + */ + public function responses(): Responses + { + return new Responses($this->transporter); + } + /** * Given a prompt, the model will return one or more predicted completions, and can also return the probabilities * of alternative tokens at each position. diff --git a/src/Contracts/ClientContract.php b/src/Contracts/ClientContract.php index ad018605..daf44729 100644 --- a/src/Contracts/ClientContract.php +++ b/src/Contracts/ClientContract.php @@ -15,6 +15,7 @@ use OpenAI\Contracts\Resources\ImagesContract; use OpenAI\Contracts\Resources\ModelsContract; use OpenAI\Contracts\Resources\ModerationsContract; +use OpenAI\Contracts\Resources\ResponsesContract; use OpenAI\Contracts\Resources\ThreadsContract; use OpenAI\Contracts\Resources\VectorStoresContract; @@ -28,6 +29,13 @@ interface ClientContract */ public function completions(): CompletionsContract; + /** + * Manage responses to assist models with tasks. + * + * @see https://platform.openai.com/docs/api-reference/responses + */ + public function responses(): ResponsesContract; + /** * Given a chat conversation, the model will return a chat completion response. * diff --git a/src/Contracts/Resources/ResponsesContract.php b/src/Contracts/Resources/ResponsesContract.php new file mode 100644 index 00000000..4d13d6ed --- /dev/null +++ b/src/Contracts/Resources/ResponsesContract.php @@ -0,0 +1,60 @@ + $parameters + */ + public function create(array $parameters): CreateResponse; + + /** + * Create a streamed response. + * + * @see https://platform.openai.com/docs/api-reference/responses/create + * + * @param array $parameters + * @return StreamResponse + */ + public function createStreamed(array $parameters): StreamResponse; + + /** + * Retrieves a model response with the given ID. + * + * @see https://platform.openai.com/docs/api-reference/responses/retrieve + */ + public function retrieve(string $id): RetrieveResponse; + + /** + * Deletes a model response with the given ID. + * + * @see https://platform.openai.com/docs/api-reference/responses/delete + */ + public function delete(string $id): DeleteResponse; + + /** + * Returns a list of input items for a given response. + * + * @see https://platform.openai.com/docs/api-reference/responses/input-items + * + * @param array $parameters + */ + public function list(string $id, array $parameters = []): ListInputItems; +} diff --git a/src/Resources/Responses.php b/src/Resources/Responses.php new file mode 100644 index 00000000..c31357b3 --- /dev/null +++ b/src/Resources/Responses.php @@ -0,0 +1,114 @@ + $parameters + */ + public function create(array $parameters): CreateResponse + { + $this->ensureNotStreamed($parameters); + + $payload = Payload::create('responses', $parameters); + + /** @var Response $response */ + $response = $this->transporter->requestObject($payload); + + return CreateResponse::from($response->data(), $response->meta()); + } + + /** + * When you create a Response with stream set to true, + * the server will emit server-sent events to the client as the Response is generated. + * + * @see https://platform.openai.com/docs/api-reference/responses-streaming + * + * @param array $parameters + * @return StreamResponse + */ + public function createStreamed(array $parameters): StreamResponse + { + $parameters = $this->setStreamParameter($parameters); + + $payload = Payload::create('responses', $parameters); + + $response = $this->transporter->requestStream($payload); + + return new StreamResponse(CreateStreamedResponse::class, $response); + } + + /** + * Retrieves a model response with the given ID. + * + * @see https://platform.openai.com/docs/api-reference/responses/get + */ + public function retrieve(string $id): RetrieveResponse + { + $payload = Payload::retrieve('responses', $id); + + /** @var Response $response */ + $response = $this->transporter->requestObject($payload); + + return RetrieveResponse::from($response->data(), $response->meta()); + } + + /** + * Deletes a model response with the given ID. + * + * @see https://platform.openai.com/docs/api-reference/responses/delete + */ + public function delete(string $id): DeleteResponse + { + $payload = Payload::delete('responses', $id); + + /** @var Response $response */ + $response = $this->transporter->requestObject($payload); + + return DeleteResponse::from($response->data(), $response->meta()); + } + + /** + * Lists input items for a response with the given ID. + * + * @see https://platform.openai.com/docs/api-reference/responses/input-items + * + * @param array $parameters + */ + public function list(string $id, array $parameters = []): ListInputItems + { + $payload = Payload::list('responses/'.$id.'/input_items', $parameters); + + /** @var Response $response */ + $response = $this->transporter->requestObject($payload); + + return ListInputItems::from($response->data(), $response->meta()); + } +} diff --git a/src/Responses/Responses/CreateResponse.php b/src/Responses/Responses/CreateResponse.php new file mode 100644 index 00000000..a803dadf --- /dev/null +++ b/src/Responses/Responses/CreateResponse.php @@ -0,0 +1,208 @@ + + * @phpstan-type OutputType array + * @phpstan-type CreateResponseType array{id: string, object: 'response', created_at: int, status: 'completed'|'failed'|'in_progress'|'incomplete', error: ErrorType|null, incomplete_details: IncompleteDetailsType|null, instructions: string|null, max_output_tokens: int|null, model: string, output: OutputType, parallel_tool_calls: bool, previous_response_id: string|null, reasoning: ReasoningType|null, store: bool, temperature: float|null, text: ResponseFormatType, tool_choice: ToolChoiceType, tools: ToolsType, top_p: float|null, truncation: 'auto'|'disabled'|null, usage: UsageType|null, user: string|null, metadata: array|null} + * + * @implements ResponseContract + */ +final class CreateResponse implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + /** + * @param 'response' $object + * @param 'completed'|'failed'|'in_progress'|'incomplete' $status + * @param array $output + * @param array $tools + * @param 'auto'|'disabled'|null $truncation + * @param array $metadata + */ + private function __construct( + public readonly string $id, + public readonly string $object, + public readonly int $createdAt, + public readonly string $status, + public readonly ?CreateResponseError $error, + public readonly ?CreateResponseIncompleteDetails $incompleteDetails, + public readonly ?string $instructions, + public readonly ?int $maxOutputTokens, + public readonly string $model, + public readonly array $output, + public readonly bool $parallelToolCalls, + public readonly ?string $previousResponseId, + public readonly ?CreateResponseReasoning $reasoning, + public readonly bool $store, + public readonly ?float $temperature, + public readonly CreateResponseFormat $text, + public readonly string|FunctionToolChoice|HostedToolChoice $toolChoice, + public readonly array $tools, + public readonly ?float $topP, + public readonly ?string $truncation, + public readonly ?CreateResponseUsage $usage, + public readonly ?string $user, + public readonly array $metadata, + private readonly MetaInformation $meta, + ) {} + + /** + * @param CreateResponseType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + $output = array_map( + fn (array $output): OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning => match ($output['type']) { + 'message' => OutputMessage::from($output), + 'file_search_call' => OutputFileSearchToolCall::from($output), + 'function_call' => OutputFunctionToolCall::from($output), + 'web_search_call' => OutputWebSearchToolCall::from($output), + 'computer_call' => OutputComputerToolCall::from($output), + 'reasoning' => OutputReasoning::from($output), + }, + $attributes['output'], + ); + + $toolChoice = is_array($attributes['tool_choice']) + ? match ($attributes['tool_choice']['type']) { + 'file_search', 'web_search_preview', 'computer_use_preview' => HostedToolChoice::from($attributes['tool_choice']), + 'function' => FunctionToolChoice::from($attributes['tool_choice']), + } + : $attributes['tool_choice']; + + $tools = array_map( + fn (array $tool): ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool => match ($tool['type']) { + 'file_search' => FileSearchTool::from($tool), + 'web_search_preview', 'web_search_preview_2025_03_11' => WebSearchTool::from($tool), + 'function' => FunctionTool::from($tool), + 'computer_use_preview' => ComputerUseTool::from($tool), + }, + $attributes['tools'], + ); + + return new self( + id: $attributes['id'], + object: $attributes['object'], + createdAt: $attributes['created_at'], + status: $attributes['status'], + error: isset($attributes['error']) + ? CreateResponseError::from($attributes['error']) + : null, + incompleteDetails: isset($attributes['incomplete_details']) + ? CreateResponseIncompleteDetails::from($attributes['incomplete_details']) + : null, + instructions: $attributes['instructions'], + maxOutputTokens: $attributes['max_output_tokens'], + model: $attributes['model'], + output: $output, + parallelToolCalls: $attributes['parallel_tool_calls'], + previousResponseId: $attributes['previous_response_id'], + reasoning: isset($attributes['reasoning']) + ? CreateResponseReasoning::from($attributes['reasoning']) + : null, + store: $attributes['store'], + temperature: $attributes['temperature'], + text: CreateResponseFormat::from($attributes['text']), + toolChoice: $toolChoice, + tools: $tools, + topP: $attributes['top_p'], + truncation: $attributes['truncation'], + usage: isset($attributes['usage']) + ? CreateResponseUsage::from($attributes['usage']) + : null, + user: $attributes['user'] ?? null, + metadata: $attributes['metadata'] ?? [], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + // https://github.com/phpstan/phpstan/issues/8438 + // @phpstan-ignore-next-line + return [ + 'id' => $this->id, + 'object' => $this->object, + 'created_at' => $this->createdAt, + 'status' => $this->status, + 'error' => $this->error?->toArray(), + 'incomplete_details' => $this->incompleteDetails?->toArray(), + 'instructions' => $this->instructions, + 'max_output_tokens' => $this->maxOutputTokens, + 'metadata' => $this->metadata ?? [], + 'model' => $this->model, + 'output' => array_map( + fn (OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning $output): array => $output->toArray(), + $this->output + ), + 'parallel_tool_calls' => $this->parallelToolCalls, + 'previous_response_id' => $this->previousResponseId, + 'reasoning' => $this->reasoning?->toArray(), + 'store' => $this->store, + 'temperature' => $this->temperature, + 'text' => $this->text->toArray(), + 'tool_choice' => is_string($this->toolChoice) + ? $this->toolChoice + : $this->toolChoice->toArray(), + 'tools' => array_map( + fn (ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool $tool): array => $tool->toArray(), + $this->tools + ), + 'top_p' => $this->topP, + 'truncation' => $this->truncation, + 'usage' => $this->usage?->toArray(), + 'user' => $this->user, + ]; + } +} diff --git a/src/Responses/Responses/CreateResponseError.php b/src/Responses/Responses/CreateResponseError.php new file mode 100644 index 00000000..42e1e625 --- /dev/null +++ b/src/Responses/Responses/CreateResponseError.php @@ -0,0 +1,51 @@ + + */ +final class CreateResponseError implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly string $code, + public readonly string $message + ) {} + + /** + * @param ErrorType $attributes + */ + public static function from(array $attributes): self + { + return new self( + code: $attributes['code'], + message: $attributes['message'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'code' => $this->code, + 'message' => $this->message, + ]; + } +} diff --git a/src/Responses/Responses/CreateResponseFormat.php b/src/Responses/Responses/CreateResponseFormat.php new file mode 100644 index 00000000..c00cd479 --- /dev/null +++ b/src/Responses/Responses/CreateResponseFormat.php @@ -0,0 +1,61 @@ + + */ +final class CreateResponseFormat implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly TextFormat|JsonSchemaFormat|JsonObjectFormat $format + ) {} + + /** + * @param ResponseFormatType $attributes + */ + public static function from(array $attributes): self + { + $format = match ($attributes['format']['type']) { + 'text' => TextFormat::from($attributes['format']), + 'json_schema' => JsonSchemaFormat::from($attributes['format']), + 'json_object' => JsonObjectFormat::from($attributes['format']), + }; + + return new self( + format: $format + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'format' => $this->format->toArray(), + ]; + } +} diff --git a/src/Responses/Responses/CreateResponseIncompleteDetails.php b/src/Responses/Responses/CreateResponseIncompleteDetails.php new file mode 100644 index 00000000..f6b1d5ea --- /dev/null +++ b/src/Responses/Responses/CreateResponseIncompleteDetails.php @@ -0,0 +1,48 @@ + + */ +final class CreateResponseIncompleteDetails implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly string $reason, + ) {} + + /** + * @param IncompleteDetailsType $attributes + */ + public static function from(array $attributes): self + { + return new self( + reason: $attributes['reason'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'reason' => $this->reason, + ]; + } +} diff --git a/src/Responses/Responses/CreateResponseReasoning.php b/src/Responses/Responses/CreateResponseReasoning.php new file mode 100644 index 00000000..cf41dbbf --- /dev/null +++ b/src/Responses/Responses/CreateResponseReasoning.php @@ -0,0 +1,51 @@ + + */ +final class CreateResponseReasoning implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly ?string $effort, + public readonly ?string $generate_summary, + ) {} + + /** + * @param ReasoningType $attributes + */ + public static function from(array $attributes): self + { + return new self( + effort: $attributes['effort'] ?? null, + generate_summary: $attributes['generate_summary'] ?? null, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'effort' => $this->effort, + 'generate_summary' => $this->generate_summary, + ]; + } +} diff --git a/src/Responses/Responses/CreateResponseUsage.php b/src/Responses/Responses/CreateResponseUsage.php new file mode 100644 index 00000000..551b2fc4 --- /dev/null +++ b/src/Responses/Responses/CreateResponseUsage.php @@ -0,0 +1,63 @@ + + */ +final class CreateResponseUsage implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly int $inputTokens, + public readonly CreateResponseUsageInputTokenDetails $inputTokensDetails, + public readonly int $outputTokens, + public readonly CreateResponseUsageOutputTokenDetails $outputTokensDetails, + public readonly int $totalTokens, + ) {} + + /** + * @param UsageType $attributes + */ + public static function from(array $attributes): self + { + return new self( + inputTokens: $attributes['input_tokens'], + inputTokensDetails: CreateResponseUsageInputTokenDetails::from($attributes['input_tokens_details']), + outputTokens: $attributes['output_tokens'], + outputTokensDetails: CreateResponseUsageOutputTokenDetails::from($attributes['output_tokens_details']), + totalTokens: $attributes['total_tokens'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'input_tokens' => $this->inputTokens, + 'input_tokens_details' => $this->inputTokensDetails->toArray(), + 'output_tokens' => $this->outputTokens, + 'output_tokens_details' => $this->outputTokensDetails->toArray(), + 'total_tokens' => $this->totalTokens, + ]; + } +} diff --git a/src/Responses/Responses/CreateResponseUsageInputTokenDetails.php b/src/Responses/Responses/CreateResponseUsageInputTokenDetails.php new file mode 100644 index 00000000..2e7c9693 --- /dev/null +++ b/src/Responses/Responses/CreateResponseUsageInputTokenDetails.php @@ -0,0 +1,48 @@ + + */ +final class CreateResponseUsageInputTokenDetails implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly int $cachedTokens, + ) {} + + /** + * @param InputTokenDetailsType $attributes + */ + public static function from(array $attributes): self + { + return new self( + cachedTokens: $attributes['cached_tokens'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'cached_tokens' => $this->cachedTokens, + ]; + } +} diff --git a/src/Responses/Responses/CreateResponseUsageOutputTokenDetails.php b/src/Responses/Responses/CreateResponseUsageOutputTokenDetails.php new file mode 100644 index 00000000..db83ca79 --- /dev/null +++ b/src/Responses/Responses/CreateResponseUsageOutputTokenDetails.php @@ -0,0 +1,48 @@ + + */ +final class CreateResponseUsageOutputTokenDetails implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly int $reasoningTokens, + ) {} + + /** + * @param OutputTokenDetailsType $attributes + */ + public static function from(array $attributes): self + { + return new self( + reasoningTokens: $attributes['reasoning_tokens'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'reasoning_tokens' => $this->reasoningTokens, + ]; + } +} diff --git a/src/Responses/Responses/CreateStreamedResponse.php b/src/Responses/Responses/CreateStreamedResponse.php new file mode 100644 index 00000000..33010858 --- /dev/null +++ b/src/Responses/Responses/CreateStreamedResponse.php @@ -0,0 +1,102 @@ +} + * + * @implements ResponseContract + */ +final class CreateStreamedResponse implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use FakeableForStreamedResponse; + + private function __construct( + public readonly string $event, + public readonly CreateResponse|OutputItem|ContentPart|OutputTextDelta|OutputTextAnnotationAdded|OutputTextDone|RefusalDelta|RefusalDone|FunctionCallArgumentsDelta|FunctionCallArgumentsDone|FileSearchCall|WebSearchCall|ReasoningSummaryPart|ReasoningSummaryTextDelta|ReasoningSummaryTextDone|Error $response, + ) {} + + /** + * @param array $attributes + */ + public static function from(array $attributes): self + { + $event = $attributes['type'] ?? throw new UnknownEventException('Missing event type in streamed response'); + $meta = $attributes['__meta']; + unset($attributes['__meta']); + + $response = match ($event) { + 'response.created', + 'response.in_progress', + 'response.completed', + 'response.failed', + 'response.incomplete' => CreateResponse::from($attributes['response'], $meta), // @phpstan-ignore-line + 'response.output_item.added', + 'response.output_item.done' => OutputItem::from($attributes, $meta), // @phpstan-ignore-line + 'response.content_part.added', + 'response.content_part.done' => ContentPart::from($attributes, $meta), // @phpstan-ignore-line + 'response.output_text.delta' => OutputTextDelta::from($attributes, $meta), // @phpstan-ignore-line + 'response.output_text.done' => OutputTextDone::from($attributes, $meta), // @phpstan-ignore-line + 'response.output_text.annotation.added' => OutputTextAnnotationAdded::from($attributes, $meta), // @phpstan-ignore-line + 'response.refusal.delta' => RefusalDelta::from($attributes, $meta), // @phpstan-ignore-line + 'response.refusal.done' => RefusalDone::from($attributes, $meta), // @phpstan-ignore-line + 'response.function_call_arguments.delta' => FunctionCallArgumentsDelta::from($attributes, $meta), // @phpstan-ignore-line + 'response.function_call_arguments.done' => FunctionCallArgumentsDone::from($attributes, $meta), // @phpstan-ignore-line + 'response.file_search_call.in_progress', + 'response.file_search_call.searching', + 'response.file_search_call.completed' => FileSearchCall::from($attributes, $meta), // @phpstan-ignore-line + 'response.web_search_call.in_progress', + 'response.web_search_call.searching', + 'response.web_search_call.completed' => WebSearchCall::from($attributes, $meta), // @phpstan-ignore-line + 'response.reasoning_summary_part.added', + 'response.reasoning_summary_part.done' => ReasoningSummaryPart::from($attributes, $meta), // @phpstan-ignore-line + 'response.reasoning_summary_text.delta' => ReasoningSummaryTextDelta::from($attributes, $meta), // @phpstan-ignore-line + 'response.reasoning_summary_text.done' => ReasoningSummaryTextDone::from($attributes, $meta), // @phpstan-ignore-line + 'error' => Error::from($attributes, $meta), // @phpstan-ignore-line + default => throw new UnknownEventException('Unknown Responses streaming event: '.$event), + }; + + return new self( + event: $event, // @phpstan-ignore-line + response: $response, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'event' => $this->event, + 'data' => $this->response->toArray(), + ]; + } +} diff --git a/src/Responses/Responses/DeleteResponse.php b/src/Responses/Responses/DeleteResponse.php new file mode 100644 index 00000000..8732903f --- /dev/null +++ b/src/Responses/Responses/DeleteResponse.php @@ -0,0 +1,60 @@ + + */ +final class DeleteResponse implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + private function __construct( + public readonly string $id, + public readonly string $object, + public readonly bool $deleted, + private readonly MetaInformation $meta, + ) {} + + /** + * @param DeleteResponseType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + return new self( + id: $attributes['id'], + object: $attributes['object'], + deleted: $attributes['deleted'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'object' => $this->object, + 'deleted' => $this->deleted, + ]; + } +} diff --git a/src/Responses/Responses/Format/JsonObjectFormat.php b/src/Responses/Responses/Format/JsonObjectFormat.php new file mode 100644 index 00000000..38cb5307 --- /dev/null +++ b/src/Responses/Responses/Format/JsonObjectFormat.php @@ -0,0 +1,51 @@ + + */ +final class JsonObjectFormat implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'json_object' $type + */ + private function __construct( + public readonly string $type, + ) {} + + /** + * @param JsonObjectFormatType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Format/JsonSchemaFormat.php b/src/Responses/Responses/Format/JsonSchemaFormat.php new file mode 100644 index 00000000..3f2f22fe --- /dev/null +++ b/src/Responses/Responses/Format/JsonSchemaFormat.php @@ -0,0 +1,64 @@ +, type: 'json_schema', description: ?string, strict: ?bool} + * + * @implements ResponseContract + */ +final class JsonSchemaFormat implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $schema + * @param 'json_schema' $type + */ + private function __construct( + public readonly string $name, + public readonly array $schema, + public readonly string $type, + public readonly ?string $description, + public readonly ?bool $strict = null, + ) {} + + /** + * @param JsonSchemaFormatType $attributes + */ + public static function from(array $attributes): self + { + return new self( + name: $attributes['name'], + schema: $attributes['schema'], + type: $attributes['type'], + description: $attributes['description'] ?? null, + strict: $attributes['strict'] ?? null, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'name' => $this->name, + 'schema' => $this->schema, + 'type' => $this->type, + 'description' => $this->description, + 'strict' => $this->strict, + ]; + } +} diff --git a/src/Responses/Responses/Format/TextFormat.php b/src/Responses/Responses/Format/TextFormat.php new file mode 100644 index 00000000..8aa9e43f --- /dev/null +++ b/src/Responses/Responses/Format/TextFormat.php @@ -0,0 +1,51 @@ + + */ +final class TextFormat implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'text' $type + */ + private function __construct( + public readonly string $type, + ) {} + + /** + * @param TextFormatType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Input/AcknowledgedSafetyCheck.php b/src/Responses/Responses/Input/AcknowledgedSafetyCheck.php new file mode 100644 index 00000000..aa482752 --- /dev/null +++ b/src/Responses/Responses/Input/AcknowledgedSafetyCheck.php @@ -0,0 +1,54 @@ + + */ +final class AcknowledgedSafetyCheck implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly string $code, + public readonly string $id, + public readonly string $message, + ) {} + + /** + * @param AcknowledgedSafetyCheckType $attributes + */ + public static function from(array $attributes): self + { + return new self( + code: $attributes['code'], + id: $attributes['id'], + message: $attributes['message'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'code' => $this->code, + 'id' => $this->id, + 'message' => $this->message, + ]; + } +} diff --git a/src/Responses/Responses/Input/ComputerToolCallOutput.php b/src/Responses/Responses/Input/ComputerToolCallOutput.php new file mode 100644 index 00000000..eeb7ff2d --- /dev/null +++ b/src/Responses/Responses/Input/ComputerToolCallOutput.php @@ -0,0 +1,79 @@ +, status: 'in_progress'|'completed'|'incomplete'} + * + * @implements ResponseContract + */ +final class ComputerToolCallOutput implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'computer_call_output' $type + * @param array $acknowledgedSafetyChecks + * @param 'in_progress'|'completed'|'incomplete' $status + */ + private function __construct( + public readonly string $callId, + public readonly string $id, + public readonly ComputerToolCallOutputScreenshot $output, + public readonly string $type, + public readonly array $acknowledgedSafetyChecks, + public readonly string $status, + ) {} + + /** + * @param ComputerToolCallOutputType $attributes + */ + public static function from(array $attributes): self + { + $acknowledgedSafetyChecks = array_map( + fn (array $acknowledgedSafetyCheck) => AcknowledgedSafetyCheck::from($acknowledgedSafetyCheck), + $attributes['acknowledged_safety_checks'], + ); + + return new self( + callId: $attributes['call_id'], + id: $attributes['id'], + output: ComputerToolCallOutputScreenshot::from($attributes['output']), + type: $attributes['type'], + acknowledgedSafetyChecks: $acknowledgedSafetyChecks, + status: $attributes['status'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'call_id' => $this->callId, + 'id' => $this->id, + 'output' => $this->output->toArray(), + 'type' => $this->type, + 'acknowledged_safety_checks' => array_map( + fn (AcknowledgedSafetyCheck $acknowledgedSafetyCheck) => $acknowledgedSafetyCheck->toArray(), + $this->acknowledgedSafetyChecks, + ), + 'status' => $this->status, + ]; + } +} diff --git a/src/Responses/Responses/Input/ComputerToolCallOutputScreenshot.php b/src/Responses/Responses/Input/ComputerToolCallOutputScreenshot.php new file mode 100644 index 00000000..d56a2ccd --- /dev/null +++ b/src/Responses/Responses/Input/ComputerToolCallOutputScreenshot.php @@ -0,0 +1,57 @@ + + */ +final class ComputerToolCallOutputScreenshot implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'computer_screenshot' $type + */ + private function __construct( + public readonly string $type, + public readonly string $fileId, + public readonly string $imageUrl, + ) {} + + /** + * @param ComputerToolCallOutputScreenshotType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + fileId: $attributes['file_id'], + imageUrl: $attributes['image_url'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'file_id' => $this->fileId, + 'image_url' => $this->imageUrl, + ]; + } +} diff --git a/src/Responses/Responses/Input/FunctionToolCallOutput.php b/src/Responses/Responses/Input/FunctionToolCallOutput.php new file mode 100644 index 00000000..267e7232 --- /dev/null +++ b/src/Responses/Responses/Input/FunctionToolCallOutput.php @@ -0,0 +1,64 @@ + + */ +final class FunctionToolCallOutput implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'function_call_output' $type + * @param 'in_progress'|'completed'|'incompleted' $status + */ + private function __construct( + public readonly string $callId, + public readonly string $id, + public readonly string $output, + public readonly string $type, + public readonly string $status, + ) {} + + /** + * @param FunctionToolCallOutputType $attributes + */ + public static function from(array $attributes): self + { + return new self( + callId: $attributes['call_id'], + id: $attributes['id'], + output: $attributes['output'], + type: $attributes['type'], + status: $attributes['status'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'call_id' => $this->callId, + 'id' => $this->id, + 'output' => $this->output, + 'type' => $this->type, + 'status' => $this->status, + ]; + } +} diff --git a/src/Responses/Responses/Input/InputMessage.php b/src/Responses/Responses/Input/InputMessage.php new file mode 100644 index 00000000..140786fe --- /dev/null +++ b/src/Responses/Responses/Input/InputMessage.php @@ -0,0 +1,82 @@ +, id: string, role: 'user'|'system'|'developer', status: 'in_progress'|'completed'|'incomplete', type: 'message'} + * + * @implements ResponseContract + */ +final class InputMessage implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $content + * @param 'user'|'system'|'developer' $role + * @param 'in_progress'|'completed'|'incomplete' $status + * @param 'message' $type + */ + private function __construct( + public readonly array $content, + public readonly string $id, + public readonly string $role, + public readonly string $status, + public readonly string $type, + ) {} + + /** + * @param InputMessageType $attributes + */ + public static function from(array $attributes): self + { + $content = array_map( + fn (array $item): InputMessageContentInputText|InputMessageContentInputImage|InputMessageContentInputFile => match ($item['type']) { + 'input_text' => InputMessageContentInputText::from($item), + 'input_image' => InputMessageContentInputImage::from($item), + 'input_file' => InputMessageContentInputFile::from($item), + }, + $attributes['content'], + ); + + return new self( + content: $content, + id: $attributes['id'], + role: $attributes['role'], + status: $attributes['status'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'content' => array_map( + fn (InputMessageContentInputText|InputMessageContentInputImage|InputMessageContentInputFile $item): array => $item->toArray(), + $this->content, + ), + 'id' => $this->id, + 'role' => $this->role, + 'status' => $this->status, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Input/InputMessageContentInputFile.php b/src/Responses/Responses/Input/InputMessageContentInputFile.php new file mode 100644 index 00000000..1b1b8ca2 --- /dev/null +++ b/src/Responses/Responses/Input/InputMessageContentInputFile.php @@ -0,0 +1,60 @@ + + */ +final class InputMessageContentInputFile implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'input_file' $type + */ + private function __construct( + public readonly string $type, + public readonly string $fileData, + public readonly string $fileId, + public readonly string $filename, + ) {} + + /** + * @param ContentInputFileType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + fileData: $attributes['file_data'], + fileId: $attributes['file_id'], + filename: $attributes['filename'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'file_data' => $this->fileData, + 'file_id' => $this->fileId, + 'filename' => $this->filename, + ]; + } +} diff --git a/src/Responses/Responses/Input/InputMessageContentInputImage.php b/src/Responses/Responses/Input/InputMessageContentInputImage.php new file mode 100644 index 00000000..2cdec2f6 --- /dev/null +++ b/src/Responses/Responses/Input/InputMessageContentInputImage.php @@ -0,0 +1,60 @@ + + */ +final class InputMessageContentInputImage implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'input_image' $type + */ + private function __construct( + public readonly string $type, + public readonly string $detail, + public readonly ?string $fileId, + public readonly ?string $imageUrl, + ) {} + + /** + * @param ContentInputImageType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + detail: $attributes['detail'], + fileId: $attributes['file_id'], + imageUrl: $attributes['image_url'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'detail' => $this->detail, + 'file_id' => $this->fileId, + 'image_url' => $this->imageUrl, + ]; + } +} diff --git a/src/Responses/Responses/Input/InputMessageContentInputText.php b/src/Responses/Responses/Input/InputMessageContentInputText.php new file mode 100644 index 00000000..e7934eab --- /dev/null +++ b/src/Responses/Responses/Input/InputMessageContentInputText.php @@ -0,0 +1,54 @@ + + */ +final class InputMessageContentInputText implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'input_text' $type + */ + private function __construct( + public readonly string $text, + public readonly string $type + ) {} + + /** + * @param ContentInputTextType $attributes + */ + public static function from(array $attributes): self + { + return new self( + text: $attributes['text'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'text' => $this->text, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/ListInputItems.php b/src/Responses/Responses/ListInputItems.php new file mode 100644 index 00000000..caff4ecb --- /dev/null +++ b/src/Responses/Responses/ListInputItems.php @@ -0,0 +1,101 @@ +, first_id: string, has_more: bool, last_id: string, object: 'list'} + * + * @implements ResponseContract + */ +final class ListInputItems implements ResponseContract, ResponseHasMetaInformationContract +{ + /** @use ArrayAccessible */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + /** + * @param array $data + * @param 'list' $object + */ + private function __construct( + public readonly string $object, + public readonly array $data, + public readonly string $firstId, + public readonly string $lastId, + public readonly bool $hasMore, + private readonly MetaInformation $meta, + ) {} + + /** + * @param ListInputItemsType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + $data = array_map( + fn (array $item): InputMessage|OutputMessage|OutputFileSearchToolCall|OutputComputerToolCall|ComputerToolCallOutput|OutputWebSearchToolCall|OutputFunctionToolCall|FunctionToolCallOutput => match ($item['type']) { + 'message' => $item['role'] === 'assistant' ? OutputMessage::from($item) : InputMessage::from($item), + 'file_search_call' => OutputFileSearchToolCall::from($item), + 'function_call' => OutputFunctionToolCall::from($item), + 'function_call_output' => FunctionToolCallOutput::from($item), + 'web_search_call' => OutputWebSearchToolCall::from($item), + 'computer_call' => OutputComputerToolCall::from($item), + 'computer_call_output' => ComputerToolCallOutput::from($item), + }, + $attributes['data'], + ); + + return new self( + object: $attributes['object'], + data: $data, + firstId: $attributes['first_id'], + lastId: $attributes['last_id'], + hasMore: $attributes['has_more'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'object' => $this->object, + 'data' => array_map( + fn (InputMessage|OutputMessage|OutputFileSearchToolCall|OutputComputerToolCall|ComputerToolCallOutput|OutputWebSearchToolCall|OutputFunctionToolCall|FunctionToolCallOutput $item): array => $item->toArray(), + $this->data, + ), + 'first_id' => $this->firstId, + 'last_id' => $this->lastId, + 'has_more' => $this->hasMore, + ]; + } +} diff --git a/src/Responses/Responses/Output/ComputerAction/OutputComputerActionClick.php b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionClick.php new file mode 100644 index 00000000..6f2a26d0 --- /dev/null +++ b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionClick.php @@ -0,0 +1,61 @@ + + */ +final class OutputComputerActionClick implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'left'|'right'|'wheel'|'back'|'forward' $button + * @param 'click' $type + */ + private function __construct( + public readonly string $button, + public readonly string $type, + public readonly int $x, + public readonly int $y, + ) {} + + /** + * @param ClickType $attributes + */ + public static function from(array $attributes): self + { + return new self( + button: $attributes['button'], + type: $attributes['type'], + x: $attributes['x'], + y: $attributes['y'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'button' => $this->button, + 'type' => $this->type, + 'x' => $this->x, + 'y' => $this->y, + ]; + } +} diff --git a/src/Responses/Responses/Output/ComputerAction/OutputComputerActionDoubleClick.php b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionDoubleClick.php new file mode 100644 index 00000000..1706b051 --- /dev/null +++ b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionDoubleClick.php @@ -0,0 +1,57 @@ + + */ +final class OutputComputerActionDoubleClick implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'double_click' $type + */ + private function __construct( + public readonly string $type, + public readonly float $x, + public readonly float $y, + ) {} + + /** + * @param DoubleClickType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + x: $attributes['x'], + y: $attributes['y'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'x' => $this->x, + 'y' => $this->y, + ]; + } +} diff --git a/src/Responses/Responses/Output/ComputerAction/OutputComputerActionDrag.php b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionDrag.php new file mode 100644 index 00000000..7ec17910 --- /dev/null +++ b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionDrag.php @@ -0,0 +1,65 @@ +, type: 'drag'} + * + * @implements ResponseContract + */ +final class OutputComputerActionDrag implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $path + * @param 'drag' $type + */ + private function __construct( + public readonly array $path, + public readonly string $type, + ) {} + + /** + * @param DragType $attributes + */ + public static function from(array $attributes): self + { + $paths = array_map( + static fn (array $path): OutputComputerDragPath => OutputComputerDragPath::from($path), + $attributes['path'], + ); + + return new self( + path: $paths, + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'path' => array_map( + static fn (OutputComputerDragPath $path): array => $path->toArray(), + $this->path, + ), + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Output/ComputerAction/OutputComputerActionKeyPress.php b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionKeyPress.php new file mode 100644 index 00000000..a1561f45 --- /dev/null +++ b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionKeyPress.php @@ -0,0 +1,55 @@ +, type: 'keypress'} + * + * @implements ResponseContract + */ +final class OutputComputerActionKeyPress implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $keys + * @param 'keypress' $type + */ + private function __construct( + public readonly array $keys, + public readonly string $type, + ) {} + + /** + * @param KeyPressType $attributes + */ + public static function from(array $attributes): self + { + return new self( + keys: $attributes['keys'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'keys' => $this->keys, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Output/ComputerAction/OutputComputerActionMove.php b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionMove.php new file mode 100644 index 00000000..1a6fd48f --- /dev/null +++ b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionMove.php @@ -0,0 +1,57 @@ + + */ +final class OutputComputerActionMove implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'move' $type + */ + private function __construct( + public readonly string $type, + public readonly int $x, + public readonly int $y, + ) {} + + /** + * @param MoveType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + x: $attributes['x'], + y: $attributes['y'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'x' => $this->x, + 'y' => $this->y, + ]; + } +} diff --git a/src/Responses/Responses/Output/ComputerAction/OutputComputerActionScreenshot.php b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionScreenshot.php new file mode 100644 index 00000000..8138d322 --- /dev/null +++ b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionScreenshot.php @@ -0,0 +1,51 @@ + + */ +final class OutputComputerActionScreenshot implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'screenshot' $type + */ + private function __construct( + public readonly string $type, + ) {} + + /** + * @param ScreenshotType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Output/ComputerAction/OutputComputerActionScroll.php b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionScroll.php new file mode 100644 index 00000000..d4560366 --- /dev/null +++ b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionScroll.php @@ -0,0 +1,63 @@ + + */ +final class OutputComputerActionScroll implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'scroll' $type + */ + private function __construct( + public readonly int $scrollX, + public readonly int $scrollY, + public readonly string $type, + public readonly int $x, + public readonly int $y, + ) {} + + /** + * @param ScrollType $attributes + */ + public static function from(array $attributes): self + { + return new self( + scrollX: $attributes['scroll_x'], + scrollY: $attributes['scroll_y'], + type: $attributes['type'], + x: $attributes['x'], + y: $attributes['y'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'scroll_x' => $this->scrollX, + 'scroll_y' => $this->scrollY, + 'type' => $this->type, + 'x' => $this->x, + 'y' => $this->y, + ]; + } +} diff --git a/src/Responses/Responses/Output/ComputerAction/OutputComputerActionType.php b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionType.php new file mode 100644 index 00000000..a7d6d1db --- /dev/null +++ b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionType.php @@ -0,0 +1,54 @@ + + */ +final class OutputComputerActionType implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'type' $type + */ + private function __construct( + public readonly string $text, + public readonly string $type, + ) {} + + /** + * @param TypeType $attributes + */ + public static function from(array $attributes): self + { + return new self( + text: $attributes['text'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'text' => $this->text, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Output/ComputerAction/OutputComputerActionWait.php b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionWait.php new file mode 100644 index 00000000..36770c37 --- /dev/null +++ b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionWait.php @@ -0,0 +1,51 @@ + + */ +final class OutputComputerActionWait implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'wait' $type + */ + private function __construct( + public readonly string $type, + ) {} + + /** + * @param WaitType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Output/ComputerAction/OutputComputerDragPath.php b/src/Responses/Responses/Output/ComputerAction/OutputComputerDragPath.php new file mode 100644 index 00000000..9d8594b9 --- /dev/null +++ b/src/Responses/Responses/Output/ComputerAction/OutputComputerDragPath.php @@ -0,0 +1,51 @@ + + */ +final class OutputComputerDragPath implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly int $x, + public readonly int $y, + ) {} + + /** + * @param DragPathType $attributes + */ + public static function from(array $attributes): self + { + return new self( + x: $attributes['x'], + y: $attributes['y'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'x' => $this->x, + 'y' => $this->y, + ]; + } +} diff --git a/src/Responses/Responses/Output/ComputerAction/OutputComputerPendingSafetyCheck.php b/src/Responses/Responses/Output/ComputerAction/OutputComputerPendingSafetyCheck.php new file mode 100644 index 00000000..13d2b178 --- /dev/null +++ b/src/Responses/Responses/Output/ComputerAction/OutputComputerPendingSafetyCheck.php @@ -0,0 +1,54 @@ + + */ +final class OutputComputerPendingSafetyCheck implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly string $code, + public readonly string $id, + public readonly string $message, + ) {} + + /** + * @param PendingSafetyCheckType $attributes + */ + public static function from(array $attributes): self + { + return new self( + code: $attributes['code'], + id: $attributes['id'], + message: $attributes['message'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'code' => $this->code, + 'id' => $this->id, + 'message' => $this->message, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputComputerToolCall.php b/src/Responses/Responses/Output/OutputComputerToolCall.php new file mode 100644 index 00000000..cb598e53 --- /dev/null +++ b/src/Responses/Responses/Output/OutputComputerToolCall.php @@ -0,0 +1,109 @@ +, status: 'in_progress'|'completed'|'incomplete', type: 'computer_call'} + * + * @implements ResponseContract + */ +final class OutputComputerToolCall implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $pendingSafetyChecks + * @param 'in_progress'|'completed'|'incomplete' $status + * @param 'computer_call' $type + */ + private function __construct( + public readonly Click|DoubleClick|Drag|KeyPress|Move|Screenshot|Scroll|Type|Wait $action, + public readonly string $callId, + public readonly string $id, + public readonly array $pendingSafetyChecks, + public readonly string $status, + public readonly string $type, + ) {} + + /** + * @param OutputComputerToolCallType $attributes + */ + public static function from(array $attributes): self + { + $action = match ($attributes['action']['type']) { + 'click' => Click::from($attributes['action']), + 'double_click' => DoubleClick::from($attributes['action']), + 'drag' => Drag::from($attributes['action']), + 'keypress' => KeyPress::from($attributes['action']), + 'move' => Move::from($attributes['action']), + 'screenshot' => Screenshot::from($attributes['action']), + 'scroll' => Scroll::from($attributes['action']), + 'type' => Type::from($attributes['action']), + 'wait' => Wait::from($attributes['action']), + }; + + $pendingSafetyChecks = array_map( + fn (array $safetyCheck): OutputComputerPendingSafetyCheck => OutputComputerPendingSafetyCheck::from($safetyCheck), + $attributes['pending_safety_checks'] + ); + + return new self( + action: $action, + callId: $attributes['call_id'], + id: $attributes['id'], + pendingSafetyChecks: $pendingSafetyChecks, + status: $attributes['status'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'call_id' => $this->callId, + 'id' => $this->id, + 'action' => $this->action->toArray(), + 'pending_safety_checks' => array_map( + fn (OutputComputerPendingSafetyCheck $safetyCheck): array => $safetyCheck->toArray(), + $this->pendingSafetyChecks, + ), + 'status' => $this->status, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputFileSearchToolCall.php b/src/Responses/Responses/Output/OutputFileSearchToolCall.php new file mode 100644 index 00000000..961d9565 --- /dev/null +++ b/src/Responses/Responses/Output/OutputFileSearchToolCall.php @@ -0,0 +1,78 @@ +, status: 'in_progress'|'searching'|'incomplete'|'failed', type: 'file_search_call', results: ?array} + * + * @implements ResponseContract + */ +final class OutputFileSearchToolCall implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $queries + * @param 'in_progress'|'searching'|'incomplete'|'failed' $status + * @param 'file_search_call' $type + * @param ?array $results + */ + private function __construct( + public readonly string $id, + public readonly array $queries, + public readonly string $status, + public readonly string $type, + public readonly ?array $results = null, + ) {} + + /** + * @param OutputFileSearchToolCallType $attributes + */ + public static function from(array $attributes): self + { + $results = isset($attributes['results']) + ? array_map( + fn (array $result): OutputFileSearchToolCallResult => OutputFileSearchToolCallResult::from($result), + $attributes['results'] + ) + : null; + + return new self( + id: $attributes['id'], + queries: $attributes['queries'], + status: $attributes['status'], + type: $attributes['type'], + results: $results, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'queries' => $this->queries, + 'status' => $this->status, + 'type' => $this->type, + 'results' => isset($this->results) ? array_map( + fn (OutputFileSearchToolCallResult $result) => $result->toArray(), + $this->results + ) : null, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputFileSearchToolCallResult.php b/src/Responses/Responses/Output/OutputFileSearchToolCallResult.php new file mode 100644 index 00000000..7576c01a --- /dev/null +++ b/src/Responses/Responses/Output/OutputFileSearchToolCallResult.php @@ -0,0 +1,63 @@ +, file_id: string, filename: string, score: float, text: string} + * + * @implements ResponseContract + */ +final class OutputFileSearchToolCallResult implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $attributes + */ + private function __construct( + public readonly array $attributes, + public readonly string $fileId, + public readonly string $filename, + public readonly float $score, + public readonly string $text, + ) {} + + /** + * @param OutputFileSearchToolCallResultType $attributes + */ + public static function from(array $attributes): self + { + return new self( + attributes: $attributes['attributes'], + fileId: $attributes['file_id'], + filename: $attributes['filename'], + score: $attributes['score'], + text: $attributes['text'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'attributes' => $this->attributes, + 'file_id' => $this->fileId, + 'filename' => $this->filename, + 'score' => $this->score, + 'text' => $this->text, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputFunctionToolCall.php b/src/Responses/Responses/Output/OutputFunctionToolCall.php new file mode 100644 index 00000000..985fa9d2 --- /dev/null +++ b/src/Responses/Responses/Output/OutputFunctionToolCall.php @@ -0,0 +1,67 @@ + + */ +final class OutputFunctionToolCall implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'function_call' $type + * @param 'in_progress'|'completed'|'incomplete' $status + */ + private function __construct( + public readonly string $arguments, + public readonly string $callId, + public readonly string $name, + public readonly string $type, + public readonly string $id, + public readonly string $status, + ) {} + + /** + * @param OutputFunctionToolCallType $attributes + */ + public static function from(array $attributes): self + { + return new self( + arguments: $attributes['arguments'], + callId: $attributes['call_id'], + name: $attributes['name'], + type: $attributes['type'], + id: $attributes['id'], + status: $attributes['status'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'arguments' => $this->arguments, + 'call_id' => $this->callId, + 'name' => $this->name, + 'type' => $this->type, + 'id' => $this->id, + 'status' => $this->status, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputMessage.php b/src/Responses/Responses/Output/OutputMessage.php new file mode 100644 index 00000000..dfa957e2 --- /dev/null +++ b/src/Responses/Responses/Output/OutputMessage.php @@ -0,0 +1,80 @@ +, id: string, role: 'assistant', status: 'in_progress'|'completed'|'incomplete', type: 'message'} + * + * @implements ResponseContract + */ +final class OutputMessage implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $content + * @param 'assistant' $role + * @param 'in_progress'|'completed'|'incomplete' $status + * @param 'message' $type + */ + private function __construct( + public readonly array $content, + public readonly string $id, + public readonly string $role, + public readonly string $status, + public readonly string $type, + ) {} + + /** + * @param OutputMessageType $attributes + */ + public static function from(array $attributes): self + { + $content = array_map( + fn (array $item): OutputMessageContentOutputText|OutputMessageContentRefusal => match ($item['type']) { + 'output_text' => OutputMessageContentOutputText::from($item), + 'refusal' => OutputMessageContentRefusal::from($item), + }, + $attributes['content'], + ); + + return new self( + content: $content, + id: $attributes['id'], + role: $attributes['role'], + status: $attributes['status'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'content' => array_map( + fn (OutputMessageContentOutputText|OutputMessageContentRefusal $item): array => $item->toArray(), + $this->content, + ), + 'id' => $this->id, + 'role' => $this->role, + 'status' => $this->status, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputMessageContentOutputText.php b/src/Responses/Responses/Output/OutputMessageContentOutputText.php new file mode 100644 index 00000000..77eedbfe --- /dev/null +++ b/src/Responses/Responses/Output/OutputMessageContentOutputText.php @@ -0,0 +1,77 @@ +, text: string, type: 'output_text'} + * + * @implements ResponseContract + */ +final class OutputMessageContentOutputText implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $annotations + * @param 'output_text' $type + */ + private function __construct( + public readonly array $annotations, + public readonly string $text, + public readonly string $type + ) {} + + /** + * @param OutputTextType $attributes + */ + public static function from(array $attributes): self + { + $annotations = array_map( + fn (array $annotation): AnnotationFileCitation|AnnotationFilePath|AnnotationUrlCitation => match ($annotation['type']) { + 'file_citation' => AnnotationFileCitation::from($annotation), + 'file_path' => AnnotationFilePath::from($annotation), + 'url_citation' => AnnotationUrlCitation::from($annotation), + }, + $attributes['annotations'], + ); + + return new self( + annotations: $annotations, + text: $attributes['text'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'annotations' => array_map( + fn (AnnotationFileCitation|AnnotationFilePath|AnnotationUrlCitation $annotation): array => $annotation->toArray(), + $this->annotations, + ), + 'text' => $this->text, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsFileCitation.php b/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsFileCitation.php new file mode 100644 index 00000000..073e3f35 --- /dev/null +++ b/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsFileCitation.php @@ -0,0 +1,57 @@ + + */ +final class OutputMessageContentOutputTextAnnotationsFileCitation implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'file_citation' $type + */ + private function __construct( + public readonly string $fileId, + public readonly int $index, + public readonly string $type, + ) {} + + /** + * @param array{file_id: string, index: int, type: 'file_citation'} $attributes + */ + public static function from(array $attributes): self + { + return new self( + fileId: $attributes['file_id'], + index: $attributes['index'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'file_id' => $this->fileId, + 'index' => $this->index, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsFilePath.php b/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsFilePath.php new file mode 100644 index 00000000..11272c46 --- /dev/null +++ b/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsFilePath.php @@ -0,0 +1,57 @@ + + */ +final class OutputMessageContentOutputTextAnnotationsFilePath implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'file_path' $type + */ + private function __construct( + public readonly string $fileId, + public readonly int $index, + public readonly string $type, + ) {} + + /** + * @param FilePathType $attributes + */ + public static function from(array $attributes): self + { + return new self( + fileId: $attributes['file_id'], + index: $attributes['index'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'file_id' => $this->fileId, + 'index' => $this->index, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsUrlCitation.php b/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsUrlCitation.php new file mode 100644 index 00000000..469fd2de --- /dev/null +++ b/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsUrlCitation.php @@ -0,0 +1,63 @@ + + */ +final class OutputMessageContentOutputTextAnnotationsUrlCitation implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'url_citation' $type + */ + private function __construct( + public readonly int $endIndex, + public readonly int $startIndex, + public readonly string $title, + public readonly string $type, + public readonly string $url, + ) {} + + /** + * @param UrlCitationType $attributes + */ + public static function from(array $attributes): self + { + return new self( + endIndex: $attributes['end_index'], + startIndex: $attributes['start_index'], + title: $attributes['title'], + type: $attributes['type'], + url: $attributes['url'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'end_index' => $this->endIndex, + 'start_index' => $this->startIndex, + 'title' => $this->title, + 'type' => $this->type, + 'url' => $this->url, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputMessageContentRefusal.php b/src/Responses/Responses/Output/OutputMessageContentRefusal.php new file mode 100644 index 00000000..b19d72c0 --- /dev/null +++ b/src/Responses/Responses/Output/OutputMessageContentRefusal.php @@ -0,0 +1,54 @@ + + */ +final class OutputMessageContentRefusal implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'refusal' $type + */ + private function __construct( + public readonly string $refusal, + public readonly string $type, + ) {} + + /** + * @param ContentRefusalType $attributes + */ + public static function from(array $attributes): self + { + return new self( + refusal: $attributes['refusal'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'refusal' => $this->refusal, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputReasoning.php b/src/Responses/Responses/Output/OutputReasoning.php new file mode 100644 index 00000000..8a4c3fb6 --- /dev/null +++ b/src/Responses/Responses/Output/OutputReasoning.php @@ -0,0 +1,75 @@ +, type: 'reasoning', encrypted_content: string|null, status?: 'in_progress'|'completed'|'incomplete'|null} + * + * @implements ResponseContract + */ +final class OutputReasoning implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $summary + * @param 'reasoning' $type + * @param 'in_progress'|'completed'|'incomplete'|null $status + */ + private function __construct( + public readonly string $id, + public readonly array $summary, + public readonly string $type, + public readonly ?string $encryptedContent, + public readonly ?string $status, + ) {} + + /** + * @param OutputReasoningType $attributes + */ + public static function from(array $attributes): self + { + $summary = array_map( + static fn (array $summary): OutputReasoningSummary => OutputReasoningSummary::from($summary), + $attributes['summary'], + ); + + return new self( + id: $attributes['id'], + summary: $summary, + type: $attributes['type'], + encryptedContent: $attributes['encrypted_content'] ?? null, + status: $attributes['status'] ?? null, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'summary' => array_map( + static fn (OutputReasoningSummary $summary): array => $summary->toArray(), + $this->summary, + ), + 'type' => $this->type, + 'encrypted_content' => $this->encryptedContent, + 'status' => $this->status, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputReasoningSummary.php b/src/Responses/Responses/Output/OutputReasoningSummary.php new file mode 100644 index 00000000..5c80ed81 --- /dev/null +++ b/src/Responses/Responses/Output/OutputReasoningSummary.php @@ -0,0 +1,54 @@ + + */ +final class OutputReasoningSummary implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'summary_text' $type + */ + private function __construct( + public readonly string $text, + public readonly string $type, + ) {} + + /** + * @param ReasoningSummaryType $attributes + */ + public static function from(array $attributes): self + { + return new self( + text: $attributes['text'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'text' => $this->text, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputWebSearchToolCall.php b/src/Responses/Responses/Output/OutputWebSearchToolCall.php new file mode 100644 index 00000000..1fe2fadc --- /dev/null +++ b/src/Responses/Responses/Output/OutputWebSearchToolCall.php @@ -0,0 +1,57 @@ + + */ +final class OutputWebSearchToolCall implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'web_search_call' $type + */ + private function __construct( + public readonly string $id, + public readonly string $status, + public readonly string $type, + ) {} + + /** + * @param OutputWebSearchToolCallType $attributes + */ + public static function from(array $attributes): self + { + return new self( + id: $attributes['id'], + status: $attributes['status'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'status' => $this->status, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/RetrieveResponse.php b/src/Responses/Responses/RetrieveResponse.php new file mode 100644 index 00000000..35902b80 --- /dev/null +++ b/src/Responses/Responses/RetrieveResponse.php @@ -0,0 +1,208 @@ + + * @phpstan-type OutputType array + * @phpstan-type RetrieveResponseType array{id: string, object: 'response', created_at: int, status: 'completed'|'failed'|'in_progress'|'incomplete', error: ErrorType|null, incomplete_details: IncompleteDetailsType|null, instructions: string|null, max_output_tokens: int|null, model: string, output: OutputType, parallel_tool_calls: bool, previous_response_id: string|null, reasoning: ReasoningType|null, store: bool, temperature: float|null, text: ResponseFormatType, tool_choice: ToolChoiceType, tools: ToolsType, top_p: float|null, truncation: 'auto'|'disabled'|null, usage: UsageType|null, user: string|null, metadata: array|null} + * + * @implements ResponseContract + */ +final class RetrieveResponse implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + /** + * @param 'response' $object + * @param 'completed'|'failed'|'in_progress'|'incomplete' $status + * @param array $output + * @param array $tools + * @param 'auto'|'disabled'|null $truncation + * @param array $metadata + */ + private function __construct( + public readonly string $id, + public readonly string $object, + public readonly int $createdAt, + public readonly string $status, + public readonly ?CreateResponseError $error, + public readonly ?CreateResponseIncompleteDetails $incompleteDetails, + public readonly ?string $instructions, + public readonly ?int $maxOutputTokens, + public readonly string $model, + public readonly array $output, + public readonly bool $parallelToolCalls, + public readonly ?string $previousResponseId, + public readonly ?CreateResponseReasoning $reasoning, + public readonly bool $store, + public readonly ?float $temperature, + public readonly CreateResponseFormat $text, + public readonly string|FunctionToolChoice|HostedToolChoice $toolChoice, + public readonly array $tools, + public readonly ?float $topP, + public readonly ?string $truncation, + public readonly ?CreateResponseUsage $usage, + public readonly ?string $user, + public array $metadata, + private readonly MetaInformation $meta, + ) {} + + /** + * @param RetrieveResponseType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + $output = array_map( + fn (array $output): OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning => match ($output['type']) { + 'message' => OutputMessage::from($output), + 'file_search_call' => OutputFileSearchToolCall::from($output), + 'function_call' => OutputFunctionToolCall::from($output), + 'web_search_call' => OutputWebSearchToolCall::from($output), + 'computer_call' => OutputComputerToolCall::from($output), + 'reasoning' => OutputReasoning::from($output), + }, + $attributes['output'], + ); + + $toolChoice = is_array($attributes['tool_choice']) + ? match ($attributes['tool_choice']['type']) { + 'file_search', 'web_search_preview', 'computer_use_preview' => HostedToolChoice::from($attributes['tool_choice']), + 'function' => FunctionToolChoice::from($attributes['tool_choice']), + } + : $attributes['tool_choice']; + + $tools = array_map( + fn (array $tool): ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool => match ($tool['type']) { + 'file_search' => FileSearchTool::from($tool), + 'web_search_preview', 'web_search_preview_2025_03_11' => WebSearchTool::from($tool), + 'function' => FunctionTool::from($tool), + 'computer_use_preview' => ComputerUseTool::from($tool), + }, + $attributes['tools'], + ); + + return new self( + id: $attributes['id'], + object: $attributes['object'], + createdAt: $attributes['created_at'], + status: $attributes['status'], + error: isset($attributes['error']) + ? CreateResponseError::from($attributes['error']) + : null, + incompleteDetails: isset($attributes['incomplete_details']) + ? CreateResponseIncompleteDetails::from($attributes['incomplete_details']) + : null, + instructions: $attributes['instructions'], + maxOutputTokens: $attributes['max_output_tokens'], + model: $attributes['model'], + output: $output, + parallelToolCalls: $attributes['parallel_tool_calls'], + previousResponseId: $attributes['previous_response_id'], + reasoning: isset($attributes['reasoning']) + ? CreateResponseReasoning::from($attributes['reasoning']) + : null, + store: $attributes['store'], + temperature: $attributes['temperature'], + text: CreateResponseFormat::from($attributes['text']), + toolChoice: $toolChoice, + tools: $tools, + topP: $attributes['top_p'], + truncation: $attributes['truncation'], + usage: isset($attributes['usage']) + ? CreateResponseUsage::from($attributes['usage']) + : null, + user: $attributes['user'] ?? null, + metadata: $attributes['metadata'] ?? [], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + // https://github.com/phpstan/phpstan/issues/8438 + // @phpstan-ignore-next-line + return [ + 'id' => $this->id, + 'object' => $this->object, + 'created_at' => $this->createdAt, + 'status' => $this->status, + 'error' => $this->error?->toArray(), + 'incomplete_details' => $this->incompleteDetails?->toArray(), + 'instructions' => $this->instructions, + 'max_output_tokens' => $this->maxOutputTokens, + 'metadata' => $this->metadata ?? [], + 'model' => $this->model, + 'output' => array_map( + fn (OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning $output): array => $output->toArray(), + $this->output + ), + 'parallel_tool_calls' => $this->parallelToolCalls, + 'previous_response_id' => $this->previousResponseId, + 'reasoning' => $this->reasoning?->toArray(), + 'store' => $this->store, + 'temperature' => $this->temperature, + 'text' => $this->text->toArray(), + 'tool_choice' => is_string($this->toolChoice) + ? $this->toolChoice + : $this->toolChoice->toArray(), + 'tools' => array_map( + fn (ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool $tool): array => $tool->toArray(), + $this->tools + ), + 'top_p' => $this->topP, + 'truncation' => $this->truncation, + 'usage' => $this->usage?->toArray(), + 'user' => $this->user, + ]; + } +} diff --git a/src/Responses/Responses/Streaming/ContentPart.php b/src/Responses/Responses/Streaming/ContentPart.php new file mode 100644 index 00000000..2ffab5b5 --- /dev/null +++ b/src/Responses/Responses/Streaming/ContentPart.php @@ -0,0 +1,73 @@ + + */ +final class ContentPart implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + private function __construct( + public readonly int $contentIndex, + public readonly string $itemId, + public readonly int $outputIndex, + public readonly OutputMessageContentOutputText|OutputMessageContentRefusal $part, + private readonly MetaInformation $meta, + ) {} + + /** + * @param ContentPartType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + $part = match ($attributes['part']['type']) { + 'output_text' => OutputMessageContentOutputText::from($attributes['part']), + 'refusal' => OutputMessageContentRefusal::from($attributes['part']), + }; + + return new self( + contentIndex: $attributes['content_index'], + itemId: $attributes['item_id'], + outputIndex: $attributes['output_index'], + part: $part, + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'content_index' => $this->contentIndex, + 'item_id' => $this->itemId, + 'output_index' => $this->outputIndex, + 'part' => $this->part->toArray(), + ]; + } +} diff --git a/src/Responses/Responses/Streaming/Error.php b/src/Responses/Responses/Streaming/Error.php new file mode 100644 index 00000000..fe713440 --- /dev/null +++ b/src/Responses/Responses/Streaming/Error.php @@ -0,0 +1,60 @@ + + */ +final class Error implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + private function __construct( + public readonly ?string $code, + public readonly string $message, + public readonly ?string $param, + private readonly MetaInformation $meta, + ) {} + + /** + * @param ErrorType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + return new self( + code: $attributes['code'], + message: $attributes['message'], + param: $attributes['param'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'code' => $this->code, + 'message' => $this->message, + 'param' => $this->param, + ]; + } +} diff --git a/src/Responses/Responses/Streaming/FileSearchCall.php b/src/Responses/Responses/Streaming/FileSearchCall.php new file mode 100644 index 00000000..63ddd926 --- /dev/null +++ b/src/Responses/Responses/Streaming/FileSearchCall.php @@ -0,0 +1,57 @@ + + */ +final class FileSearchCall implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + private function __construct( + public readonly string $itemId, + public readonly int $outputIndex, + private readonly MetaInformation $meta, + ) {} + + /** + * @param FileSearchCallType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + return new self( + itemId: $attributes['item_id'], + outputIndex: $attributes['output_index'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'item_id' => $this->itemId, + 'output_index' => $this->outputIndex, + ]; + } +} diff --git a/src/Responses/Responses/Streaming/FunctionCallArgumentsDelta.php b/src/Responses/Responses/Streaming/FunctionCallArgumentsDelta.php new file mode 100644 index 00000000..ae756041 --- /dev/null +++ b/src/Responses/Responses/Streaming/FunctionCallArgumentsDelta.php @@ -0,0 +1,60 @@ + + */ +final class FunctionCallArgumentsDelta implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + private function __construct( + public readonly string $delta, + public readonly string $itemId, + public readonly int $outputIndex, + private readonly MetaInformation $meta, + ) {} + + /** + * @param FunctionCallArgumentsDeltaType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + return new self( + delta: $attributes['delta'], + itemId: $attributes['item_id'], + outputIndex: $attributes['output_index'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'delta' => $this->delta, + 'item_id' => $this->itemId, + 'output_index' => $this->outputIndex, + ]; + } +} diff --git a/src/Responses/Responses/Streaming/FunctionCallArgumentsDone.php b/src/Responses/Responses/Streaming/FunctionCallArgumentsDone.php new file mode 100644 index 00000000..ce21fb55 --- /dev/null +++ b/src/Responses/Responses/Streaming/FunctionCallArgumentsDone.php @@ -0,0 +1,60 @@ + + */ +final class FunctionCallArgumentsDone implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + private function __construct( + public readonly string $arguments, + public readonly string $itemId, + public readonly int $outputIndex, + private readonly MetaInformation $meta, + ) {} + + /** + * @param FunctionCallArgumentsDoneType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + return new self( + arguments: $attributes['arguments'], + itemId: $attributes['item_id'], + outputIndex: $attributes['output_index'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'arguments' => $this->arguments, + 'item_id' => $this->itemId, + 'output_index' => $this->outputIndex, + ]; + } +} diff --git a/src/Responses/Responses/Streaming/OutputItem.php b/src/Responses/Responses/Streaming/OutputItem.php new file mode 100644 index 00000000..d1324a7b --- /dev/null +++ b/src/Responses/Responses/Streaming/OutputItem.php @@ -0,0 +1,79 @@ + + */ +final class OutputItem implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + private function __construct( + public readonly int $outputIndex, + public readonly OutputMessage|OutputFileSearchToolCall|OutputFunctionToolCall|OutputWebSearchToolCall|OutputComputerToolCall|OutputReasoning $item, + private readonly MetaInformation $meta, + ) {} + + /** + * @param OutputItemType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + $item = match ($attributes['item']['type']) { + 'message' => OutputMessage::from($attributes['item']), + 'file_search_call' => OutputFileSearchToolCall::from($attributes['item']), + 'function_call' => OutputFunctionToolCall::from($attributes['item']), + 'web_search_call' => OutputWebSearchToolCall::from($attributes['item']), + 'computer_call' => OutputComputerToolCall::from($attributes['item']), + 'reasoning' => OutputReasoning::from($attributes['item']), + }; + + return new self( + outputIndex: $attributes['output_index'], + item: $item, + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'output_index' => $this->outputIndex, + 'item' => $this->item->toArray(), + ]; + } +} diff --git a/src/Responses/Responses/Streaming/OutputTextAnnotationAdded.php b/src/Responses/Responses/Streaming/OutputTextAnnotationAdded.php new file mode 100644 index 00000000..0f8aeed2 --- /dev/null +++ b/src/Responses/Responses/Streaming/OutputTextAnnotationAdded.php @@ -0,0 +1,79 @@ + + */ +final class OutputTextAnnotationAdded implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + private function __construct( + public readonly OutputMessageContentOutputTextAnnotationsFileCitation|OutputMessageContentOutputTextAnnotationsFilePath|OutputMessageContentOutputTextAnnotationsUrlCitation $annotation, + public readonly int $annotationIndex, + public readonly int $contentIndex, + public readonly string $itemId, + public readonly int $outputIndex, + private readonly MetaInformation $meta, + ) {} + + /** + * @param OutputTextAnnotationAddedType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + $annotation = match ($attributes['annotation']['type']) { + 'file_citation' => OutputMessageContentOutputTextAnnotationsFileCitation::from($attributes['annotation']), + 'file_path' => OutputMessageContentOutputTextAnnotationsFilePath::from($attributes['annotation']), + 'url_citation' => OutputMessageContentOutputTextAnnotationsUrlCitation::from($attributes['annotation']), + }; + + return new self( + annotation: $annotation, + annotationIndex: $attributes['annotation_index'], + contentIndex: $attributes['content_index'], + itemId: $attributes['item_id'], + outputIndex: $attributes['output_index'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'annotation' => $this->annotation->toArray(), + 'annotation_index' => $this->annotationIndex, + 'content_index' => $this->contentIndex, + 'item_id' => $this->itemId, + 'output_index' => $this->outputIndex, + ]; + } +} diff --git a/src/Responses/Responses/Streaming/OutputTextDelta.php b/src/Responses/Responses/Streaming/OutputTextDelta.php new file mode 100644 index 00000000..486eccbe --- /dev/null +++ b/src/Responses/Responses/Streaming/OutputTextDelta.php @@ -0,0 +1,63 @@ + + */ +final class OutputTextDelta implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + private function __construct( + public readonly int $contentIndex, + public readonly string $delta, + public readonly string $itemId, + public readonly int $outputIndex, + private readonly MetaInformation $meta, + ) {} + + /** + * @param OutputTextType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + return new self( + contentIndex: $attributes['content_index'], + delta: $attributes['delta'], + itemId: $attributes['item_id'], + outputIndex: $attributes['output_index'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'content_index' => $this->contentIndex, + 'delta' => $this->delta, + 'item_id' => $this->itemId, + 'output_index' => $this->outputIndex, + ]; + } +} diff --git a/src/Responses/Responses/Streaming/OutputTextDone.php b/src/Responses/Responses/Streaming/OutputTextDone.php new file mode 100644 index 00000000..7fe7e589 --- /dev/null +++ b/src/Responses/Responses/Streaming/OutputTextDone.php @@ -0,0 +1,63 @@ + + */ +final class OutputTextDone implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + private function __construct( + public readonly int $contentIndex, + public readonly string $itemId, + public readonly int $outputIndex, + public readonly string $text, + private readonly MetaInformation $meta, + ) {} + + /** + * @param OutputTextDoneType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + return new self( + contentIndex: $attributes['content_index'], + itemId: $attributes['item_id'], + outputIndex: $attributes['output_index'], + text: $attributes['text'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'content_index' => $this->contentIndex, + 'item_id' => $this->itemId, + 'output_index' => $this->outputIndex, + 'text' => $this->text, + ]; + } +} diff --git a/src/Responses/Responses/Streaming/ReasoningSummaryPart.php b/src/Responses/Responses/Streaming/ReasoningSummaryPart.php new file mode 100644 index 00000000..e39408c2 --- /dev/null +++ b/src/Responses/Responses/Streaming/ReasoningSummaryPart.php @@ -0,0 +1,66 @@ + + */ +final class ReasoningSummaryPart implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + private function __construct( + public readonly string $itemId, + public readonly int $outputIndex, + public readonly OutputReasoningSummary $part, + public readonly int $summaryIndex, + private readonly MetaInformation $meta, + ) {} + + /** + * @param ReasoningSummaryPartType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + return new self( + itemId: $attributes['item_id'], + outputIndex: $attributes['output_index'], + part: OutputReasoningSummary::from($attributes['part']), + summaryIndex: $attributes['summary_index'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'item_id' => $this->itemId, + 'output_index' => $this->outputIndex, + 'part' => $this->part->toArray(), + 'summary_index' => $this->summaryIndex, + ]; + } +} diff --git a/src/Responses/Responses/Streaming/ReasoningSummaryTextDelta.php b/src/Responses/Responses/Streaming/ReasoningSummaryTextDelta.php new file mode 100644 index 00000000..75dc420b --- /dev/null +++ b/src/Responses/Responses/Streaming/ReasoningSummaryTextDelta.php @@ -0,0 +1,63 @@ + + */ +final class ReasoningSummaryTextDelta implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + private function __construct( + public readonly string $delta, + public readonly string $itemId, + public readonly int $outputIndex, + public readonly int $summaryIndex, + private readonly MetaInformation $meta, + ) {} + + /** + * @param ReasoningSummaryTextDeltaType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + return new self( + delta: $attributes['delta'], + itemId: $attributes['item_id'], + outputIndex: $attributes['output_index'], + summaryIndex: $attributes['summary_index'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'delta' => $this->delta, + 'item_id' => $this->itemId, + 'output_index' => $this->outputIndex, + 'summary_index' => $this->summaryIndex, + ]; + } +} diff --git a/src/Responses/Responses/Streaming/ReasoningSummaryTextDone.php b/src/Responses/Responses/Streaming/ReasoningSummaryTextDone.php new file mode 100644 index 00000000..260fc1b1 --- /dev/null +++ b/src/Responses/Responses/Streaming/ReasoningSummaryTextDone.php @@ -0,0 +1,63 @@ + + */ +final class ReasoningSummaryTextDone implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + private function __construct( + public readonly string $itemId, + public readonly int $outputIndex, + public readonly int $summaryIndex, + public readonly string $text, + private readonly MetaInformation $meta, + ) {} + + /** + * @param ReasoningSummaryTextDoneType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + return new self( + itemId: $attributes['item_id'], + outputIndex: $attributes['output_index'], + summaryIndex: $attributes['summary_index'], + text: $attributes['text'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'item_id' => $this->itemId, + 'output_index' => $this->outputIndex, + 'summary_index' => $this->summaryIndex, + 'text' => $this->text, + ]; + } +} diff --git a/src/Responses/Responses/Streaming/RefusalDelta.php b/src/Responses/Responses/Streaming/RefusalDelta.php new file mode 100644 index 00000000..4791ba4a --- /dev/null +++ b/src/Responses/Responses/Streaming/RefusalDelta.php @@ -0,0 +1,63 @@ + + */ +final class RefusalDelta implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + private function __construct( + public readonly int $contentIndex, + public readonly string $delta, + public readonly string $itemId, + public readonly int $outputIndex, + private readonly MetaInformation $meta, + ) {} + + /** + * @param RefusalDeltaType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + return new self( + contentIndex: $attributes['content_index'], + delta: $attributes['delta'], + itemId: $attributes['item_id'], + outputIndex: $attributes['output_index'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'content_index' => $this->contentIndex, + 'delta' => $this->delta, + 'item_id' => $this->itemId, + 'output_index' => $this->outputIndex, + ]; + } +} diff --git a/src/Responses/Responses/Streaming/RefusalDone.php b/src/Responses/Responses/Streaming/RefusalDone.php new file mode 100644 index 00000000..47365087 --- /dev/null +++ b/src/Responses/Responses/Streaming/RefusalDone.php @@ -0,0 +1,63 @@ + + */ +final class RefusalDone implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + private function __construct( + public readonly int $contentIndex, + public readonly string $itemId, + public readonly int $outputIndex, + public readonly string $refusal, + private readonly MetaInformation $meta, + ) {} + + /** + * @param RefusalDoneType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + return new self( + contentIndex: $attributes['content_index'], + itemId: $attributes['item_id'], + outputIndex: $attributes['output_index'], + refusal: $attributes['refusal'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'content_index' => $this->contentIndex, + 'item_id' => $this->itemId, + 'output_index' => $this->outputIndex, + 'refusal' => $this->refusal, + ]; + } +} diff --git a/src/Responses/Responses/Streaming/WebSearchCall.php b/src/Responses/Responses/Streaming/WebSearchCall.php new file mode 100644 index 00000000..cb7e5a79 --- /dev/null +++ b/src/Responses/Responses/Streaming/WebSearchCall.php @@ -0,0 +1,57 @@ + + */ +final class WebSearchCall implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + private function __construct( + public readonly string $itemId, + public readonly int $outputIndex, + private readonly MetaInformation $meta, + ) {} + + /** + * @param WebSearchCallType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + return new self( + itemId: $attributes['item_id'], + outputIndex: $attributes['output_index'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'item_id' => $this->itemId, + 'output_index' => $this->outputIndex, + ]; + } +} diff --git a/src/Responses/Responses/Tool/ComputerUseTool.php b/src/Responses/Responses/Tool/ComputerUseTool.php new file mode 100644 index 00000000..6839312e --- /dev/null +++ b/src/Responses/Responses/Tool/ComputerUseTool.php @@ -0,0 +1,60 @@ + + */ +final class ComputerUseTool implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'computer_use_preview' $type + */ + private function __construct( + public readonly int $displayHeight, + public readonly int $displayWidth, + public readonly string $environment, + public readonly string $type, + ) {} + + /** + * @param ComputerUseToolType $attributes + */ + public static function from(array $attributes): self + { + return new self( + displayHeight: $attributes['display_height'], + displayWidth: $attributes['display_width'], + environment: $attributes['environment'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'display_height' => $this->displayHeight, + 'display_width' => $this->displayWidth, + 'environment' => $this->environment, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Tool/FileSearchComparisonFilter.php b/src/Responses/Responses/Tool/FileSearchComparisonFilter.php new file mode 100644 index 00000000..e0a6f67a --- /dev/null +++ b/src/Responses/Responses/Tool/FileSearchComparisonFilter.php @@ -0,0 +1,57 @@ + + */ +final class FileSearchComparisonFilter implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'eq'|'ne'|'gt'|'gte'|'lt'|'lte' $type + */ + private function __construct( + public readonly string $key, + public readonly string $type, + public readonly string|int|bool $value, + ) {} + + /** + * @param ComparisonFilterType $attributes + */ + public static function from(array $attributes): self + { + return new self( + key: $attributes['key'], + type: $attributes['type'], + value: $attributes['value'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'key' => $this->key, + 'type' => $this->type, + 'value' => $this->value, + ]; + } +} diff --git a/src/Responses/Responses/Tool/FileSearchCompoundFilter.php b/src/Responses/Responses/Tool/FileSearchCompoundFilter.php new file mode 100644 index 00000000..5eacabec --- /dev/null +++ b/src/Responses/Responses/Tool/FileSearchCompoundFilter.php @@ -0,0 +1,65 @@ +, type: 'and'|'or'} + * + * @implements ResponseContract + */ +final class FileSearchCompoundFilter implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $filters + * @param 'and'|'or' $type + */ + private function __construct( + public readonly array $filters, + public readonly string $type, + ) {} + + /** + * @param CompoundFilterType $attributes + */ + public static function from(array $attributes): self + { + $filters = array_map( + static fn (array $filter): FileSearchComparisonFilter => FileSearchComparisonFilter::from($filter), + $attributes['filters'], + ); + + return new self( + filters: $filters, + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'filters' => array_map( + static fn (FileSearchComparisonFilter $filter): array => $filter->toArray(), + $this->filters, + ), + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Tool/FileSearchRankingOption.php b/src/Responses/Responses/Tool/FileSearchRankingOption.php new file mode 100644 index 00000000..20cf72f3 --- /dev/null +++ b/src/Responses/Responses/Tool/FileSearchRankingOption.php @@ -0,0 +1,51 @@ + + */ +final class FileSearchRankingOption implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly string $ranker, + public readonly float $scoreThreshold, + ) {} + + /** + * @param RankingOptionType $attributes + */ + public static function from(array $attributes): self + { + return new self( + ranker: $attributes['ranker'], + scoreThreshold: $attributes['score_threshold'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'ranker' => $this->ranker, + 'score_threshold' => $this->scoreThreshold, + ]; + } +} diff --git a/src/Responses/Responses/Tool/FileSearchTool.php b/src/Responses/Responses/Tool/FileSearchTool.php new file mode 100644 index 00000000..72053147 --- /dev/null +++ b/src/Responses/Responses/Tool/FileSearchTool.php @@ -0,0 +1,77 @@ +, filters: ComparisonFilterType|CompoundFilterType|null, max_num_results: int, ranking_options: RankingOptionType} + * + * @implements ResponseContract + */ +final class FileSearchTool implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $vectorStoreIds + * @param 'file_search' $type + */ + private function __construct( + public readonly string $type, + public readonly array $vectorStoreIds, + public readonly FileSearchComparisonFilter|FileSearchCompoundFilter|null $filters, + public readonly int $maxNumResults, + public readonly FileSearchRankingOption $rankingOptions, + ) {} + + /** + * @param FileSearchToolType $attributes + */ + public static function from(array $attributes): self + { + $filters = null; + + if (isset($attributes['filters']['type'])) { + $filters = match ($attributes['filters']['type']) { + 'eq', 'ne', 'gt', 'gte', 'lt', 'lte' => FileSearchComparisonFilter::from($attributes['filters']), + 'and', 'or' => FileSearchCompoundFilter::from($attributes['filters']), + }; + } + + return new self( + type: $attributes['type'], + vectorStoreIds: $attributes['vector_store_ids'], + filters: $filters, + maxNumResults: $attributes['max_num_results'], + rankingOptions: FileSearchRankingOption::from($attributes['ranking_options']), + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'vector_store_ids' => $this->vectorStoreIds, + 'filters' => $this->filters?->toArray(), + 'max_num_results' => $this->maxNumResults, + 'ranking_options' => $this->rankingOptions->toArray(), + ]; + } +} diff --git a/src/Responses/Responses/Tool/FunctionTool.php b/src/Responses/Responses/Tool/FunctionTool.php new file mode 100644 index 00000000..b7b6e86d --- /dev/null +++ b/src/Responses/Responses/Tool/FunctionTool.php @@ -0,0 +1,64 @@ +, strict: bool, type: 'function', description: ?string} + * + * @implements ResponseContract + */ +final class FunctionTool implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $parameters + * @param 'function' $type + */ + private function __construct( + public readonly string $name, + public readonly array $parameters, + public readonly bool $strict, + public readonly string $type, + public readonly ?string $description = null, + ) {} + + /** + * @param FunctionToolType $attributes + */ + public static function from(array $attributes): self + { + return new self( + name: $attributes['name'], + parameters: $attributes['parameters'], + strict: $attributes['strict'], + type: $attributes['type'], + description: $attributes['description'] ?? null, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'name' => $this->name, + 'parameters' => $this->parameters, + 'strict' => $this->strict, + 'type' => $this->type, + 'description' => $this->description, + ]; + } +} diff --git a/src/Responses/Responses/Tool/WebSearchTool.php b/src/Responses/Responses/Tool/WebSearchTool.php new file mode 100644 index 00000000..c93cae06 --- /dev/null +++ b/src/Responses/Responses/Tool/WebSearchTool.php @@ -0,0 +1,62 @@ + + */ +final class WebSearchTool implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'web_search_preview'|'web_search_preview_2025_03_11' $type + * @param 'low'|'medium'|'high' $searchContextSize + */ + private function __construct( + public readonly string $type, + public readonly string $searchContextSize, + public readonly ?WebSearchUserLocation $userLocation, + ) {} + + /** + * @param WebSearchToolType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + searchContextSize: $attributes['search_context_size'], + userLocation: isset($attributes['user_location']) + ? WebSearchUserLocation::from($attributes['user_location']) + : null, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'search_context_size' => $this->searchContextSize, + 'user_location' => $this->userLocation?->toArray(), + ]; + } +} diff --git a/src/Responses/Responses/Tool/WebSearchUserLocation.php b/src/Responses/Responses/Tool/WebSearchUserLocation.php new file mode 100644 index 00000000..601726ef --- /dev/null +++ b/src/Responses/Responses/Tool/WebSearchUserLocation.php @@ -0,0 +1,63 @@ + + */ +final class WebSearchUserLocation implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'approximate' $type + */ + private function __construct( + public readonly string $type, + public readonly ?string $city, + public readonly string $country, + public readonly ?string $region, + public readonly ?string $timezone, + ) {} + + /** + * @param UserLocationType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + city: $attributes['city'], + country: $attributes['country'], + region: $attributes['region'], + timezone: $attributes['timezone'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'city' => $this->city, + 'country' => $this->country, + 'region' => $this->region, + 'timezone' => $this->timezone, + ]; + } +} diff --git a/src/Responses/Responses/ToolChoice/FunctionToolChoice.php b/src/Responses/Responses/ToolChoice/FunctionToolChoice.php new file mode 100644 index 00000000..8faf2571 --- /dev/null +++ b/src/Responses/Responses/ToolChoice/FunctionToolChoice.php @@ -0,0 +1,54 @@ + + */ +final class FunctionToolChoice implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'function' $type + */ + private function __construct( + public readonly string $name, + public readonly string $type, + ) {} + + /** + * @param FunctionToolChoiceType $attributes + */ + public static function from(array $attributes): self + { + return new self( + name: $attributes['name'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'name' => $this->name, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/ToolChoice/HostedToolChoice.php b/src/Responses/Responses/ToolChoice/HostedToolChoice.php new file mode 100644 index 00000000..3b012c13 --- /dev/null +++ b/src/Responses/Responses/ToolChoice/HostedToolChoice.php @@ -0,0 +1,51 @@ + + */ +final class HostedToolChoice implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'file_search'|'web_search_preview'|'computer_use_preview' $type + */ + private function __construct( + public readonly string $type, + ) {} + + /** + * @param HostedToolChoiceType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/StreamResponse.php b/src/Responses/StreamResponse.php index 9e2da7c2..1fa01a46 100644 --- a/src/Responses/StreamResponse.php +++ b/src/Responses/StreamResponse.php @@ -66,8 +66,8 @@ public function getIterator(): Generator if ($event !== null) { $response['__event'] = $event; - $response['__meta'] = $this->meta(); } + $response['__meta'] = $this->meta(); yield $this->responseClass::from($response); } diff --git a/src/Testing/ClientFake.php b/src/Testing/ClientFake.php index 428c5b33..c6e07481 100644 --- a/src/Testing/ClientFake.php +++ b/src/Testing/ClientFake.php @@ -21,6 +21,7 @@ use OpenAI\Testing\Resources\ImagesTestResource; use OpenAI\Testing\Resources\ModelsTestResource; use OpenAI\Testing\Resources\ModerationsTestResource; +use OpenAI\Testing\Resources\ResponsesTestResource; use OpenAI\Testing\Resources\ThreadsTestResource; use OpenAI\Testing\Resources\VectorStoresTestResource; use PHPUnit\Framework\Assert as PHPUnit; @@ -132,6 +133,11 @@ public function record(TestRequest $request): ResponseContract|ResponseStreamCon return $response; } + public function responses(): ResponsesTestResource + { + return new ResponsesTestResource($this); + } + public function completions(): CompletionsTestResource { return new CompletionsTestResource($this); diff --git a/src/Testing/Resources/ResponsesTestResource.php b/src/Testing/Resources/ResponsesTestResource.php new file mode 100644 index 00000000..7468b3a8 --- /dev/null +++ b/src/Testing/Resources/ResponsesTestResource.php @@ -0,0 +1,47 @@ +record(__FUNCTION__, func_get_args()); + } + + public function createStreamed(array $parameters): StreamResponse + { + return $this->record(__FUNCTION__, func_get_args()); + } + + public function retrieve(string $id): RetrieveResponse + { + return $this->record(__FUNCTION__, func_get_args()); + } + + public function list(string $id, array $parameters = []): ListInputItems + { + return $this->record(__FUNCTION__, func_get_args()); + } + + public function delete(string $id): DeleteResponse + { + return $this->record(__FUNCTION__, func_get_args()); + } +} diff --git a/src/Testing/Responses/Concerns/Fakeable.php b/src/Testing/Responses/Concerns/Fakeable.php index da0b117f..ca55e3ef 100644 --- a/src/Testing/Responses/Concerns/Fakeable.php +++ b/src/Testing/Responses/Concerns/Fakeable.php @@ -13,7 +13,7 @@ trait Fakeable */ public static function fake(array $override = [], ?MetaInformation $meta = null): static { - $class = str_replace('Responses\\', 'Testing\\Responses\\Fixtures\\', static::class).'Fixture'; + $class = str_replace('OpenAI\\Responses\\', 'OpenAI\\Testing\\Responses\\Fixtures\\', static::class).'Fixture'; return static::from( self::buildAttributes($class::ATTRIBUTES, $override), diff --git a/src/Testing/Responses/Fixtures/Responses/CreateResponseFixture.php b/src/Testing/Responses/Fixtures/Responses/CreateResponseFixture.php new file mode 100644 index 00000000..1d16c224 --- /dev/null +++ b/src/Testing/Responses/Fixtures/Responses/CreateResponseFixture.php @@ -0,0 +1,103 @@ + 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', + 'object' => 'response', + 'created_at' => 1741484430, + 'status' => 'completed', + 'error' => null, + 'incomplete_details' => null, + 'instructions' => null, + 'max_output_tokens' => null, + 'model' => 'gpt-4o-2024-08-06', + 'output' => [ + [ + 'type' => 'web_search_call', + 'id' => 'ws_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'status' => 'completed', + ], + [ + 'type' => 'message', + 'id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'status' => 'completed', + 'role' => 'assistant', + 'content' => [ + [ + 'type' => 'output_text', + 'text' => 'As of today, March 9, 2025, one notable positive news story...', + 'annotations' => [ + [ + 'type' => 'url_citation', + 'start_index' => 442, + 'end_index' => 557, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + [ + 'type' => 'url_citation', + 'start_index' => 962, + 'end_index' => 1077, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + [ + 'type' => 'url_citation', + 'start_index' => 1336, + 'end_index' => 1451, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + ], + ], + ], + ], + ], + 'parallel_tool_calls' => true, + 'previous_response_id' => null, + 'reasoning' => [ + 'effort' => null, + 'generate_summary' => null, + ], + 'store' => true, + 'temperature' => 1.0, + 'text' => [ + 'format' => [ + 'type' => 'text', + ], + ], + 'tool_choice' => 'auto', + 'tools' => [ + [ + 'type' => 'web_search_preview', + 'domains' => [], + 'search_context_size' => 'medium', + 'user_location' => [ + 'type' => 'approximate', + 'city' => 'San Francisco', + 'country' => 'US', + 'region' => 'California', + 'timezone' => 'America/Los_Angeles', + ], + ], + ], + 'top_p' => 1.0, + 'truncation' => 'disabled', + 'usage' => [ + 'input_tokens' => 328, + 'input_tokens_details' => [ + 'cached_tokens' => 0, + ], + 'output_tokens' => 356, + 'output_tokens_details' => [ + 'reasoning_tokens' => 0, + ], + 'total_tokens' => 684, + ], + 'user' => null, + 'metadata' => [], + ]; +} diff --git a/src/Testing/Responses/Fixtures/Responses/CreateStreamedResponseFixture.txt b/src/Testing/Responses/Fixtures/Responses/CreateStreamedResponseFixture.txt new file mode 100644 index 00000000..b7b9311e --- /dev/null +++ b/src/Testing/Responses/Fixtures/Responses/CreateStreamedResponseFixture.txt @@ -0,0 +1,9 @@ +data: {"type":"response.created","response":{"id":"resp_67c9fdcecf488190bdd9a0409de3a1ec07b8b0ad4e5eb654","object":"response","created_at":1741290958,"status":"in_progress","error":null,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} +data: {"type":"response.in_progress","response":{"id":"resp_67c9fdcecf488190bdd9a0409de3a1ec07b8b0ad4e5eb654","object":"response","created_at":1741290958,"status":"in_progress","error":null,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} +data: {"type":"response.output_item.added","output_index":0,"item":{"id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","type":"message","status":"in_progress","role":"assistant","content":[]}} +data: {"type":"response.content_part.added","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]}} +data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"Hi"} +data: {"type":"response.output_text.done","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"text":"Hi there! How can I assist you today?"} +data: {"type":"response.content_part.done","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"part":{"type":"output_text","text":"Hi there! How can I assist you today?","annotations":[]}} +data: {"type":"response.output_item.done","output_index":0,"item":{"id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Hi there! How can I assist you today?","annotations":[]}]}} +data: {"type":"response.completed","response":{"id":"resp_67c9fdcecf488190bdd9a0409de3a1ec07b8b0ad4e5eb654","object":"response","created_at":1741290958,"status":"completed","error":null,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[{"id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Hi there! How can I assist you today?","annotations":[]}]}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":37,"output_tokens":11,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":48},"user":null,"metadata":{}}} diff --git a/src/Testing/Responses/Fixtures/Responses/DeleteResponseFixture.php b/src/Testing/Responses/Fixtures/Responses/DeleteResponseFixture.php new file mode 100644 index 00000000..0ed29bba --- /dev/null +++ b/src/Testing/Responses/Fixtures/Responses/DeleteResponseFixture.php @@ -0,0 +1,12 @@ + 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', + 'object' => 'response', + 'deleted' => true, + ]; +} diff --git a/src/Testing/Responses/Fixtures/Responses/ListInputItemsFixture.php b/src/Testing/Responses/Fixtures/Responses/ListInputItemsFixture.php new file mode 100644 index 00000000..e97319c2 --- /dev/null +++ b/src/Testing/Responses/Fixtures/Responses/ListInputItemsFixture.php @@ -0,0 +1,28 @@ + 'list', + 'data' => [ + [ + 'content' => [ + [ + 'text' => 'What was a positive news story from today?', + 'type' => 'input_text', + 'annotations' => [], + ], + ], + 'id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'role' => 'user', + 'status' => 'completed', + 'type' => 'message', + ], + ], + 'first_id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'last_id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'has_more' => false, + ]; +} diff --git a/src/Testing/Responses/Fixtures/Responses/ResponseObjectFixture.php b/src/Testing/Responses/Fixtures/Responses/ResponseObjectFixture.php new file mode 100644 index 00000000..4d049b2f --- /dev/null +++ b/src/Testing/Responses/Fixtures/Responses/ResponseObjectFixture.php @@ -0,0 +1,103 @@ + 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', + 'object' => 'response', + 'created_at' => 1741484430, + 'status' => 'completed', + 'error' => null, + 'incomplete_details' => null, + 'instructions' => null, + 'max_output_tokens' => null, + 'model' => 'gpt-4o-2024-08-06', + 'output' => [ + [ + 'type' => 'web_search_call', + 'id' => 'ws_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'status' => 'completed', + ], + [ + 'type' => 'message', + 'id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'status' => 'completed', + 'role' => 'assistant', + 'content' => [ + [ + 'type' => 'output_text', + 'text' => 'As of today, March 9, 2025, one notable positive news story...', + 'annotations' => [ + [ + 'type' => 'url_citation', + 'start_index' => 442, + 'end_index' => 557, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + [ + 'type' => 'url_citation', + 'start_index' => 962, + 'end_index' => 1077, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + [ + 'type' => 'url_citation', + 'start_index' => 1336, + 'end_index' => 1451, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + ], + ], + ], + ], + ], + 'parallel_tool_calls' => true, + 'previous_response_id' => null, + 'reasoning' => [ + 'effort' => null, + 'generate_summary' => null, + ], + 'store' => true, + 'temperature' => 1.0, + 'text' => [ + 'format' => [ + 'type' => 'text', + ], + ], + 'tool_choice' => 'auto', + 'tools' => [ + [ + 'type' => 'web_search_preview', + 'domains' => [], + 'search_context_size' => 'medium', + 'user_location' => [ + 'type' => 'approximate', + 'city' => 'San Francisco', + 'country' => 'US', + 'region' => 'California', + 'timezone' => 'America/Los_Angeles', + ], + ], + ], + 'top_p' => 1.0, + 'truncation' => 'disabled', + 'usage' => [ + 'input_tokens' => 328, + 'input_tokens_details' => [ + 'cached_tokens' => 0, + ], + 'output_tokens' => 356, + 'output_tokens_details' => [ + 'reasoning_tokens' => 0, + ], + 'total_tokens' => 684, + ], + 'user' => null, + 'metadata' => [], + ]; +} diff --git a/src/Testing/Responses/Fixtures/Responses/RetrieveResponseFixture.php b/src/Testing/Responses/Fixtures/Responses/RetrieveResponseFixture.php new file mode 100644 index 00000000..9231f856 --- /dev/null +++ b/src/Testing/Responses/Fixtures/Responses/RetrieveResponseFixture.php @@ -0,0 +1,103 @@ + 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', + 'object' => 'response', + 'created_at' => 1741484430, + 'status' => 'completed', + 'error' => null, + 'incomplete_details' => null, + 'instructions' => null, + 'max_output_tokens' => null, + 'model' => 'gpt-4o-2024-08-06', + 'output' => [ + [ + 'type' => 'web_search_call', + 'id' => 'ws_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'status' => 'completed', + ], + [ + 'type' => 'message', + 'id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'status' => 'completed', + 'role' => 'assistant', + 'content' => [ + [ + 'type' => 'output_text', + 'text' => 'As of today, March 9, 2025, one notable positive news story...', + 'annotations' => [ + [ + 'type' => 'url_citation', + 'start_index' => 442, + 'end_index' => 557, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + [ + 'type' => 'url_citation', + 'start_index' => 962, + 'end_index' => 1077, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + [ + 'type' => 'url_citation', + 'start_index' => 1336, + 'end_index' => 1451, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + ], + ], + ], + ], + ], + 'parallel_tool_calls' => true, + 'previous_response_id' => null, + 'reasoning' => [ + 'effort' => null, + 'generate_summary' => null, + ], + 'store' => true, + 'temperature' => 1.0, + 'text' => [ + 'format' => [ + 'type' => 'text', + ], + ], + 'tool_choice' => 'auto', + 'tools' => [ + [ + 'type' => 'web_search_preview', + 'domains' => [], + 'search_context_size' => 'medium', + 'user_location' => [ + 'type' => 'approximate', + 'city' => 'San Francisco', + 'country' => 'US', + 'region' => 'California', + 'timezone' => 'America/Los_Angeles', + ], + ], + ], + 'top_p' => 1.0, + 'truncation' => 'disabled', + 'usage' => [ + 'input_tokens' => 328, + 'input_tokens_details' => [ + 'cached_tokens' => 0, + ], + 'output_tokens' => 356, + 'output_tokens_details' => [ + 'reasoning_tokens' => 0, + ], + 'total_tokens' => 684, + ], + 'user' => null, + 'metadata' => [], + ]; +} diff --git a/tests/Fixtures/Responses.php b/tests/Fixtures/Responses.php new file mode 100644 index 00000000..d9f0b0cd --- /dev/null +++ b/tests/Fixtures/Responses.php @@ -0,0 +1,349 @@ + + */ +function createResponseResource(): array +{ + return [ + 'id' => 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', + 'object' => 'response', + 'created_at' => 1741484430, + 'status' => 'completed', + 'error' => null, + 'incomplete_details' => null, + 'instructions' => null, + 'max_output_tokens' => null, + 'metadata' => [], + 'model' => 'gpt-4o-2024-08-06', + 'output' => [ + outputMessage(), + outputWebSearchToolCall(), + outputFileSearchToolCall(), + outputComputerToolCall(), + outputReasoning(), + ], + 'parallel_tool_calls' => true, + 'previous_response_id' => null, + 'reasoning' => [ + 'effort' => null, + 'generate_summary' => null, + ], + 'store' => true, + 'temperature' => 1.0, + 'text' => [ + 'format' => [ + 'type' => 'text', + ], + ], + 'tool_choice' => 'auto', + 'tools' => [ + toolWebSearchPreview(), + toolFileSearch(), + ], + 'top_p' => 1.0, + 'truncation' => 'disabled', + 'usage' => [ + 'input_tokens' => 328, + 'input_tokens_details' => [ + 'cached_tokens' => 0, + ], + 'output_tokens' => 356, + 'output_tokens_details' => [ + 'reasoning_tokens' => 0, + ], + 'total_tokens' => 684, + ], + 'user' => null, + ]; +} + +/** + * @return array + */ +function retrieveResponseResource(): array +{ + return [ + 'id' => 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', + 'object' => 'response', + 'created_at' => 1741484430, + 'status' => 'completed', + 'error' => null, + 'incomplete_details' => null, + 'instructions' => null, + 'max_output_tokens' => null, + 'metadata' => [], + 'model' => 'gpt-4o-2024-08-06', + 'output' => [ + outputWebSearchToolCall(), + outputMessage(), + ], + 'parallel_tool_calls' => true, + 'previous_response_id' => null, + 'reasoning' => [ + 'effort' => null, + 'generate_summary' => null, + ], + 'store' => true, + 'temperature' => 1.0, + 'text' => [ + 'format' => [ + 'type' => 'text', + ], + ], + 'tool_choice' => 'auto', + 'tools' => [ + toolWebSearchPreview(), + toolFileSearch(), + ], + 'top_p' => 1.0, + 'truncation' => 'disabled', + 'usage' => [ + 'input_tokens' => 328, + 'input_tokens_details' => [ + 'cached_tokens' => 0, + ], + 'output_tokens' => 356, + 'output_tokens_details' => [ + 'reasoning_tokens' => 0, + ], + 'total_tokens' => 684, + ], + 'user' => null, + ]; +} + +/** + * @return array + */ +function listInputItemsResource(): array +{ + return [ + 'object' => 'list', + 'data' => [ + inputMessage(), + ], + 'first_id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'last_id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'has_more' => false, + ]; +} + +/** + * @return array + */ +function deleteResponseResource(): array +{ + return [ + 'id' => 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', + 'object' => 'response.deleted', + 'deleted' => true, + ]; +} + +/** + * @return array + */ +function inputMessage(): array +{ + return [ + 'content' => [ + [ + 'text' => 'What was a positive news story from today?', + 'type' => 'input_text', + ], + ], + 'id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'role' => 'user', + 'status' => 'completed', + 'type' => 'message', + ]; +} + +/** + * @return array + */ +function outputFileSearchToolCall(): array +{ + return [ + 'id' => 'fs_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'queries' => [ + 'map', + 'kansas', + ], + 'status' => 'completed', + 'type' => 'file_search_call', + 'results' => [ + [ + 'attributes' => [ + 'foo' => 'bar', + ], + 'file_id' => 'file_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'filename' => 'kansas_map.geojson', + 'score' => 0.98882, + 'text' => 'Map of Kansas', + ], + ], + ]; +} + +/** + * @return array + */ +function outputComputerToolCall(): array +{ + return [ + 'type' => 'computer_call', + 'call_id' => 'call_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'id' => 'cu_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'action' => [ + 'button' => 'left', + 'type' => 'click', + 'x' => 117, + 'y' => 123, + ], + 'pending_safety_checks' => [ + [ + 'code' => 'malicious_instructions', + 'id' => 'cu_sc_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'message' => 'Safety check message', + ], + ], + 'status' => 'completed', + ]; +} + +/** + * @return array + */ +function outputWebSearchToolCall(): array +{ + return [ + 'id' => 'ws_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'status' => 'completed', + 'type' => 'web_search_call', + ]; +} + +/** + * @return array + */ +function outputReasoning(): array +{ + return [ + 'id' => 'rs_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'summary' => [ + [ + 'text' => 'A summary of the reasoning process.', + 'type' => 'summary_text', + ], + ], + 'type' => 'reasoning', + 'encrypted_content' => 'aabbccddeeff', + 'status' => 'completed', + ]; +} + +/** + * @return array + */ +function outputMessage(): array +{ + return [ + 'content' => [ + [ + 'annotations' => [ + [ + 'end_index' => 557, + 'start_index' => 442, + 'title' => '...', + 'type' => 'url_citation', + 'url' => 'https://.../?utm_source=chatgpt.com', + ], + [ + 'end_index' => 1077, + 'start_index' => 962, + 'title' => '...', + 'type' => 'url_citation', + 'url' => 'https://.../?utm_source=chatgpt.com', + ], + [ + 'end_index' => 1451, + 'start_index' => 1336, + 'title' => '...', + 'type' => 'url_citation', + 'url' => 'https://.../?utm_source=chatgpt.com', + ], + ], + 'text' => 'As of today, March 9, 2025, one notable positive news story...', + 'type' => 'output_text', + ], + [ + 'refusal' => 'The assistant refused to answer.', + 'type' => 'refusal', + ], + ], + 'id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'role' => 'assistant', + 'status' => 'completed', + 'type' => 'message', + ]; +} + +/** + * @return array + */ +function toolWebSearchPreview(): array +{ + return [ + 'type' => 'web_search_preview', + 'search_context_size' => 'medium', + 'user_location' => [ + 'type' => 'approximate', + 'city' => 'San Francisco', + 'country' => 'US', + 'region' => 'California', + 'timezone' => 'America/Los_Angeles', + ], + ]; +} + +/** + * @return array + */ +function toolFileSearch(): array +{ + return [ + 'type' => 'file_search', + 'vector_store_ids' => [ + 'vector_store_id_1', + 'vector_store_id_2', + ], + 'filters' => [ + 'key' => 'search-term', + 'type' => 'eq', + 'value' => 'search-term-value', + ], + 'max_num_results' => 5, + 'ranking_options' => [ + 'ranker' => 'bm25', + 'score_threshold' => 0.5, + ], + ]; +} + +/** + * @return resource + */ +function responseCompletionStream() +{ + return fopen(__DIR__.'/Streams/ResponseCompletionCreate.txt', 'r'); +} + +/** + * @return resource + */ +function responseCompletionSteamCreatedEvent() +{ + return fopen(__DIR__.'/Streams/ResponseCreatedResponse.txt', 'r'); +} diff --git a/tests/Fixtures/Streams/ResponseCompletionCreate.txt b/tests/Fixtures/Streams/ResponseCompletionCreate.txt new file mode 100644 index 00000000..e8f05b1e --- /dev/null +++ b/tests/Fixtures/Streams/ResponseCompletionCreate.txt @@ -0,0 +1,11 @@ +data: {"type":"response.created","response":{"id":"resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c","object":"response","created_at":1741484430,"status":"in_progress","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"generate_summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"web_search_preview","domains":[],"search_context_size":"medium","user_location":{"type":"approximate","city":null,"country":"US","region":null,"timezone":null}}],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} +data: {"type":"response.in_progress","response":{"id":"resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c","object":"response","created_at":1741484430,"status":"in_progress","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"generate_summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"web_search_preview","domains":[],"search_context_size":"medium","user_location":{"type":"approximate","city":null,"country":"US","region":null,"timezone":null}}],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} +data: {"type":"response.output_item.added","output_index":0,"item":{"id":"ws_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c","type":"web_search_call","status":"in_progress"}} +data: {"type":"response.output_item.done","output_index":0,"item":{"id":"ws_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c","type":"web_search_call","status":"completed"}} +data: {"type":"response.output_item.added","output_index":1,"item":{"id":"msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c","type":"message","status":"in_progress","role":"assistant","content":[]}} +data: {"type":"response.content_part.added","item_id":"msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c","output_index":1,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]}} +data: {"type":"response.output_text.delta","item_id":"msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c","output_index":1,"content_index":0,"delta":"As of today, March 9, 2025, one notable positive news story..."} +data: {"type":"response.output_text.done","item_id":"msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c","output_index":1,"content_index":0,"text":"As of today, March 9, 2025, one notable positive news story..."} +data: {"type":"response.content_part.done","item_id":"msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c","output_index":1,"content_index":0,"part":{"type":"output_text","text":"As of today, March 9, 2025, one notable positive news story...","annotations":[{"type":"url_citation","start_index":442,"end_index":557,"url":"https://.../?utm_source=chatgpt.com","title":"..."},{"type":"url_citation","start_index":962,"end_index":1077,"url":"https://.../?utm_source=chatgpt.com","title":"..."},{"type":"url_citation","start_index":1336,"end_index":1451,"url":"https://.../?utm_source=chatgpt.com","title":"..."}]}} +data: {"type":"response.output_item.done","output_index":1,"item":{"id":"msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"As of today, March 9, 2025, one notable positive news story...","annotations":[{"type":"url_citation","start_index":442,"end_index":557,"url":"https://.../?utm_source=chatgpt.com","title":"..."},{"type":"url_citation","start_index":962,"end_index":1077,"url":"https://.../?utm_source=chatgpt.com","title":"..."},{"type":"url_citation","start_index":1336,"end_index":1451,"url":"https://.../?utm_source=chatgpt.com","title":"..."}]}]}} +data: {"type":"response.completed","response":{"id":"resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c","object":"response","created_at":1741484430,"status":"completed","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[{"type":"web_search_call","id":"ws_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c","status":"completed"},{"type":"message","id":"msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c","status":"completed","role":"assistant","content":[{"type":"output_text","text":"As of today, March 9, 2025, one notable positive news story...","annotations":[{"type":"url_citation","start_index":442,"end_index":557,"url":"https://.../?utm_source=chatgpt.com","title":"..."},{"type":"url_citation","start_index":962,"end_index":1077,"url":"https://.../?utm_source=chatgpt.com","title":"..."},{"type":"url_citation","start_index":1336,"end_index":1451,"url":"https://.../?utm_source=chatgpt.com","title":"..."}]}]}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"generate_summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"web_search_preview","domains":[],"search_context_size":"medium","user_location":{"type":"approximate","city":null,"country":"US","region":null,"timezone":null}}],"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":328,"input_tokens_details":{"cached_tokens":0},"output_tokens":356,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":684},"user":null,"metadata":{}}} diff --git a/tests/Fixtures/Streams/ResponseCreatedResponse.txt b/tests/Fixtures/Streams/ResponseCreatedResponse.txt new file mode 100644 index 00000000..1d549c49 --- /dev/null +++ b/tests/Fixtures/Streams/ResponseCreatedResponse.txt @@ -0,0 +1 @@ +data: {"type":"response.created","response":{"id":"resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c","object":"response","created_at":1741484430,"status":"in_progress","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"generate_summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"web_search_preview","domains":[],"search_context_size":"medium","user_location":{"type":"approximate","city":null,"country":"US","region":null,"timezone":null}}],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} diff --git a/tests/Resources/Responses.php b/tests/Resources/Responses.php new file mode 100644 index 00000000..466114f8 --- /dev/null +++ b/tests/Resources/Responses.php @@ -0,0 +1,203 @@ + 'gpt-4o', + 'tools' => [['type' => 'web_search_preview']], + 'input' => 'what was a positive news story from today?', + ], \OpenAI\ValueObjects\Transporter\Response::from(createResponseResource(), metaHeaders())); + + $result = $client->responses()->create([ + 'model' => 'gpt-4o', + 'tools' => [['type' => 'web_search_preview']], + 'input' => 'what was a positive news story from today?', + ]); + + $output = $result->output; + expect($result) + ->toBeInstanceOf(CreateResponse::class) + ->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c') + ->object->toBe('response') + ->createdAt->toBe(1741484430) + ->status->toBe('completed') + ->error->toBeNull() + ->incompleteDetails->toBeNull() + ->instructions->toBeNull() + ->maxOutputTokens->toBeNull() + ->model->toBe('gpt-4o-2024-08-06') + ->output->toBeArray() + ->output->toHaveCount(5); + + expect($output[0]) + ->type->toBe('message') + ->id->toBe('msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c') + ->status->toBe('completed') + ->role->toBe('assistant') + ->content->toBeArray() + ->content->toHaveCount(2); + + expect($output[0]['content'][0]) + ->type->toBe('output_text') + ->text->toBe('As of today, March 9, 2025, one notable positive news story...'); + + expect($output[1]) + ->type->toBe('web_search_call') + ->id->toBe('ws_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c') + ->status->toBe('completed'); + + expect($output[4]) + ->type->toBe('reasoning'); + + expect($result) + ->parallelToolCalls->toBeTrue() + ->previousResponseId->toBeNull() + ->temperature->toBe(1.0) + ->toolChoice->toBe('auto') + ->topP->toBe(1.0) + ->truncation->toBe('disabled'); + + expect($result->truncation) + ->toBe('disabled'); + + expect($result->meta()) + ->toBeInstanceOf(MetaInformation::class); +}); + +test('create throws an exception if stream option is true', function () { + OpenAI::client('foo')->responses()->create([ + 'model' => 'gpt-3.5-turbo', + 'messages' => ['role' => 'user', 'content' => 'Hello!'], + 'stream' => true, + ]); +})->throws(OpenAI\Exceptions\InvalidArgumentException::class, 'Stream option is not supported. Please use the createStreamed() method instead.'); + +test('create streamed', function () { + $response = new Response( + body: new Stream(responseCompletionStream()), + headers: metaHeaders(), + ); + + $client = mockStreamClient('POST', 'responses', [ + 'model' => 'gpt-4o', + 'tools' => [['type' => 'web_search_preview']], + 'input' => 'what was a positive news story from today?', + 'stream' => true, + ], $response); + + $result = $client->responses()->createStreamed([ + 'model' => 'gpt-4o', + 'tools' => [['type' => 'web_search_preview']], + 'input' => 'what was a positive news story from today?', + ]); + + expect($result) + ->toBeInstanceOf(StreamResponse::class) + ->toBeInstanceOf(IteratorAggregate::class); + + expect($result->getIterator()) + ->toBeInstanceOf(Iterator::class); + + $current = $result->getIterator()->current(); + expect($current) + ->toBeInstanceOf(CreateStreamedResponse::class); + expect($current->event) + ->toBe('response.created'); + expect($current->response) + ->toBeInstanceOf(CreateResponse::class); + expect($current->response->id) + ->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); + expect($current->response->object) + ->toBe('response'); + expect($current->response->createdAt) + ->toBe(1741484430); + expect($current->response->status) + ->toBe('in_progress'); + expect($current->response->error) + ->toBeNull(); + expect($current->response->incompleteDetails) + ->toBeNull(); + expect($current->response->instructions) + ->toBeNull(); + expect($current->response->maxOutputTokens) + ->toBeNull(); + expect($current->response->model) + ->toBe('gpt-4o-2024-08-06'); + expect($current->response->output) + ->toBeArray(); + expect($current->response->output) + ->toHaveCount(0); + expect($current->response->parallelToolCalls) + ->toBeTrue(); + expect($current->response->previousResponseId) + ->toBeNull(); + expect($current->response->temperature) + ->toBe(1.0); + expect($current->response->toolChoice) + ->toBe('auto'); + expect($current->response->topP) + ->toBe(1.0); + + expect($result->meta()) + ->toBeInstanceOf(MetaInformation::class); +}); + +test('delete', function () { + $client = mockClient('DELETE', 'responses/resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', [ + ], \OpenAI\ValueObjects\Transporter\Response::from(deleteResponseResource(), metaHeaders())); + + $result = $client->responses()->delete('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); + + expect($result) + ->toBeInstanceOf(DeleteResponse::class) + ->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c') + ->object->toBe('response.deleted') + ->deleted->toBeTrue(); + + expect($result->meta()) + ->toBeInstanceOf(MetaInformation::class); +}); + +test('list', function () { + $client = mockClient('GET', 'responses/resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c/input_items', [ + ], \OpenAI\ValueObjects\Transporter\Response::from(listInputItemsResource(), metaHeaders())); + + $result = $client->responses()->list('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); + + expect($result) + ->toBeInstanceOf(ListInputItems::class) + ->object->toBe('list') + ->data->toBeArray() + ->firstId->toBe('msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c') + ->lastId->toBe('msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c') + ->hasMore->toBeFalse(); + + expect($result->meta()) + ->toBeInstanceOf(MetaInformation::class); +}); + +test('retrieve', function () { + $client = mockClient('GET', 'responses/resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', [ + ], \OpenAI\ValueObjects\Transporter\Response::from(retrieveResponseResource(), metaHeaders())); + + $result = $client->responses()->retrieve('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); + + expect($result) + ->toBeInstanceOf(RetrieveResponse::class) + ->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c') + ->object->toBe('response') + ->createdAt->toBe(1741484430) + ->status->toBe('completed'); + + expect($result->meta()) + ->toBeInstanceOf(MetaInformation::class); +}); diff --git a/tests/Responses/Responses/CreateResponse.php b/tests/Responses/Responses/CreateResponse.php new file mode 100644 index 00000000..e09e875a --- /dev/null +++ b/tests/Responses/Responses/CreateResponse.php @@ -0,0 +1,74 @@ +toBeInstanceOf(CreateResponse::class) + ->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c') + ->object->toBe('response') + ->createdAt->toBe(1741484430) + ->status->toBe('completed') + ->error->toBeNull() + ->incompleteDetails->toBeNull() + ->instructions->toBeNull() + ->maxOutputTokens->toBeNull() + ->model->toBe('gpt-4o-2024-08-06') + ->output->toBeArray() + ->parallelToolCalls->toBeTrue() + ->previousResponseId->toBeNull() + ->reasoning->toBeInstanceOf(CreateResponseReasoning::class) + ->store->toBeTrue() + ->temperature->toBe(1.0) + ->text->toBeInstanceOf(CreateResponseFormat::class) + ->toolChoice->toBe('auto') + ->tools->toBeArray() + ->topP->toBe(1.0) + ->truncation->toBe('disabled') + ->usage->toBeInstanceOf(CreateResponseUsage::class) + ->user->toBeNull() + ->metadata->toBe([]); + + expect($response->meta()) + ->toBeInstanceOf(MetaInformation::class); +}); + +test('as array accessible', function () { + $response = CreateResponse::from(createResponseResource(), meta()); + + expect($response['id'])->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); +}); + +test('to array', function () { + $response = CreateResponse::from(createResponseResource(), meta()); + + expect($response->toArray()) + ->toBeArray() + ->toBe(createResponseResource()); +}); + +test('fake', function () { + $response = CreateResponse::fake(); + + expect($response) + ->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); +}); + +test('fake with override', function () { + $response = CreateResponse::fake([ + 'id' => 'resp_1234', + 'object' => 'custom_response', + 'status' => 'failed', + ]); + + expect($response) + ->id->toBe('resp_1234') + ->object->toBe('custom_response') + ->status->toBe('failed'); +}); diff --git a/tests/Responses/Responses/CreateStreamedResponse.php b/tests/Responses/Responses/CreateStreamedResponse.php new file mode 100644 index 00000000..b718ad41 --- /dev/null +++ b/tests/Responses/Responses/CreateStreamedResponse.php @@ -0,0 +1,22 @@ +getIterator()->current()->response) + ->toBeInstanceOf(CreateResponse::class) + ->id->toBe('resp_67c9fdcecf488190bdd9a0409de3a1ec07b8b0ad4e5eb654'); +}); + +test('from', function () { + $response = CreateStreamedResponse::fake(responseCompletionSteamCreatedEvent()); + + expect($response->getIterator()->current()) + ->toBeInstanceOf(CreateStreamedResponse::class) + ->event->toBe('response.created') + ->response->toBeInstanceOf(CreateResponse::class) + ->response->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); +}); diff --git a/tests/Responses/Responses/DeleteResponse.php b/tests/Responses/Responses/DeleteResponse.php new file mode 100644 index 00000000..a6796d2d --- /dev/null +++ b/tests/Responses/Responses/DeleteResponse.php @@ -0,0 +1,47 @@ +id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c') + ->object->toBe('response.deleted') + ->deleted->toBe(true) + ->meta()->toBeInstanceOf(MetaInformation::class); +}); + +test('as array accessible', function () { + $result = DeleteResponse::from(deleteResponseResource(), meta()); + + expect($result['id']) + ->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); +}); + +test('to array', function () { + $result = DeleteResponse::from(deleteResponseResource(), meta()); + + expect($result->toArray()) + ->toBe(deleteResponseResource()); +}); + +test('fake', function () { + $response = DeleteResponse::fake(); + + expect($response) + ->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c') + ->deleted->toBe(true); +}); + +test('fake with override', function () { + $response = DeleteResponse::fake([ + 'id' => 'resp_1234', + 'deleted' => false, + ]); + + expect($response) + ->id->toBe('resp_1234') + ->deleted->toBe(false); +}); diff --git a/tests/Responses/Responses/ListInputItems.php b/tests/Responses/Responses/ListInputItems.php new file mode 100644 index 00000000..95daaf47 --- /dev/null +++ b/tests/Responses/Responses/ListInputItems.php @@ -0,0 +1,53 @@ +toBeInstanceOf(ListInputItems::class) + ->object->toBe('list') + ->data->toBeArray() + ->firstId->toBe('msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c') + ->lastId->toBe('msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c') + ->hasMore->toBeFalse() + ->meta()->toBeInstanceOf(MetaInformation::class); +}); + +test('as array accessible', function () { + $result = ListInputItems::from(listInputItemsResource(), meta()); + + expect($result['object'])->toBe('list'); +}); + +test('to array', function () { + $result = ListInputItems::from(listInputItemsResource(), meta()); + + expect($result->toArray()) + ->toBeArray() + ->toBe(listInputItemsResource()); +}); + +test('fake', function () { + $response = ListInputItems::fake(); + + expect($response) + ->object->toBe('list') + ->firstId->toBe('msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c') + ->hasMore->toBeFalse(); +}); + +test('fake with override', function () { + $response = ListInputItems::fake([ + 'object' => 'custom_list', + 'first_id' => 'msg_1234', + 'has_more' => true, + ]); + + expect($response) + ->object->toBe('custom_list') + ->firstId->toBe('msg_1234') + ->hasMore->toBeTrue(); +}); diff --git a/tests/Responses/Responses/Output/OutputComputerToolCall.php b/tests/Responses/Responses/Output/OutputComputerToolCall.php new file mode 100644 index 00000000..d4b6ba94 --- /dev/null +++ b/tests/Responses/Responses/Output/OutputComputerToolCall.php @@ -0,0 +1,30 @@ +toBeInstanceOf(OutputComputerToolCall::class) + ->action->toBeInstanceOf(OutputComputerActionClick::class) + ->callId->toBe('call_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c') + ->id->toBe('cu_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c') + ->status->toBe('completed') + ->pendingSafetyChecks->toBeArray(); +}); + +test('as array accessible', function () { + $response = OutputComputerToolCall::from(outputComputerToolCall()); + + expect($response['id'])->toBe('cu_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c'); +}); + +test('to array', function () { + $response = OutputComputerToolCall::from(outputComputerToolCall()); + + expect($response->toArray()) + ->toBeArray() + ->toBe(outputComputerToolCall()); +}); diff --git a/tests/Responses/Responses/Output/OutputFileSearchToolCall.php b/tests/Responses/Responses/Output/OutputFileSearchToolCall.php new file mode 100644 index 00000000..b92548f7 --- /dev/null +++ b/tests/Responses/Responses/Output/OutputFileSearchToolCall.php @@ -0,0 +1,42 @@ +toBeInstanceOf(OutputFileSearchToolCall::class) + ->id->toBe('fs_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c') + ->queries->toBe(['map', 'kansas']) + ->status->toBe('completed') + ->type->toBe('file_search_call') + ->results->toBeArray(); +}); + +test('from results', function () { + $response = OutputFileSearchToolCallResult::from(outputFileSearchToolCall()['results'][0]); + + expect($response) + ->toBeInstanceOf(OutputFileSearchToolCallResult::class) + ->attributes->toBe(['foo' => 'bar']) + ->fileId->toBe('file_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c') + ->filename->toBe('kansas_map.geojson') + ->score->toBe(0.98882) + ->text->toBe('Map of Kansas'); +}); + +test('as array accessible', function () { + $response = OutputFileSearchToolCall::from(outputFileSearchToolCall()); + + expect($response['id'])->toBe('fs_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c'); +}); + +test('to array', function () { + $response = OutputFileSearchToolCall::from(outputFileSearchToolCall()); + + expect($response->toArray()) + ->toBeArray() + ->toBe(outputFileSearchToolCall()); +}); diff --git a/tests/Responses/Responses/Output/OutputReasoning.php b/tests/Responses/Responses/Output/OutputReasoning.php new file mode 100644 index 00000000..168fc7df --- /dev/null +++ b/tests/Responses/Responses/Output/OutputReasoning.php @@ -0,0 +1,44 @@ +toBeInstanceOf(OutputReasoning::class) + ->summary->toBeArray() + ->id->toBe('rs_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c') + ->type->toBe('reasoning') + ->encryptedContent->toBe('aabbccddeeff') + ->status->toBe('completed'); +}); + +test('from no status', function () { + $payload = outputReasoning(); + unset($payload['status']); + + $response = OutputReasoning::from($payload); + + expect($response) + ->toBeInstanceOf(OutputReasoning::class) + ->summary->toBeArray() + ->id->toBe('rs_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c') + ->type->toBe('reasoning') + ->encryptedContent->toBe('aabbccddeeff') + ->status->toBeNull(); +}); + +test('as array accessible', function () { + $response = OutputReasoning::from(outputReasoning()); + + expect($response['id'])->toBe('rs_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c'); +}); + +test('to array', function () { + $response = OutputReasoning::from(outputReasoning()); + + expect($response->toArray()) + ->toBeArray() + ->toBe(outputReasoning()); +}); diff --git a/tests/Responses/Responses/RetrieveResponse.php b/tests/Responses/Responses/RetrieveResponse.php new file mode 100644 index 00000000..e720562f --- /dev/null +++ b/tests/Responses/Responses/RetrieveResponse.php @@ -0,0 +1,77 @@ +toBeInstanceOf(RetrieveResponse::class) + ->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c') + ->object->toBe('response') + ->createdAt->toBe(1741484430) + ->status->toBe('completed') + ->error->toBeNull() + ->incompleteDetails->toBeNull() + ->instructions->toBeNull() + ->maxOutputTokens->toBeNull() + ->model->toBe('gpt-4o-2024-08-06') + ->output->toBeArray() + ->output->toHaveCount(2) + ->parallelToolCalls->toBeTrue() + ->previousResponseId->toBeNull() + ->reasoning->toBeInstanceOf(CreateResponseReasoning::class) + ->store->toBeTrue() + ->temperature->toBe(1.0) + ->text->toBeInstanceOf(CreateResponseFormat::class) + ->toolChoice->toBe('auto') + ->tools->toBeArray() + ->tools->toHaveCount(2) + ->topP->toBe(1.0) + ->truncation->toBe('disabled') + ->usage->toBeInstanceOf(CreateResponseUsage::class) + ->user->toBeNull() + ->metadata->toBe([]); + + expect($result->meta()) + ->toBeInstanceOf(MetaInformation::class); +}); + +test('as array accessible', function () { + $result = RetrieveResponse::from(retrieveResponseResource(), meta()); + + expect($result['id'])->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); +}); + +test('to array', function () { + $result = RetrieveResponse::from(retrieveResponseResource(), meta()); + + expect($result->toArray()) + ->toBe(retrieveResponseResource()); +}); + +test('fake', function () { + $response = RetrieveResponse::fake(); + + expect($response) + ->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c') + ->object->toBe('response') + ->status->toBe('completed'); +}); + +test('fake with override', function () { + $response = RetrieveResponse::fake([ + 'id' => 'resp_1234', + 'object' => 'custom_response', + 'status' => 'failed', + ]); + + expect($response) + ->id->toBe('resp_1234') + ->object->toBe('custom_response') + ->status->toBe('failed'); +}); diff --git a/tests/Responses/Responses/Tool/FileSearchTool.php b/tests/Responses/Responses/Tool/FileSearchTool.php new file mode 100644 index 00000000..3637074d --- /dev/null +++ b/tests/Responses/Responses/Tool/FileSearchTool.php @@ -0,0 +1,54 @@ +toBeInstanceOf(FileSearchTool::class) + ->type->toBe('file_search') + ->vectorStoreIds->toBe(['vector_store_id_1', 'vector_store_id_2']) + ->filters->toBeInstanceOf(FileSearchComparisonFilter::class) + ->filters->key->toBe('search-term') + ->filters->type->toBe('eq') + ->filters->value->toBe('search-term-value') + ->maxNumResults->toBe(5) + ->rankingOptions->toBeInstanceOf(FileSearchRankingOption::class) + ->rankingOptions->ranker->toBe('bm25') + ->rankingOptions->scoreThreshold->toBe(0.5); +}); + +test('from null filters', function () { + $payload = toolFileSearch(); + $payload['filters'] = null; + $response = FileSearchTool::from($payload); + + expect($response) + ->toBeInstanceOf(FileSearchTool::class) + ->filters->toBeNull(); +}); + +test('from results', function () { + $response = FileSearchTool::from(toolFileSearch()); + + expect($response) + ->toBeInstanceOf(FileSearchTool::class) + ->type->toBe('file_search'); +}); + +test('as array accessible', function () { + $response = FileSearchTool::from(toolFileSearch()); + + expect($response['type'])->toBe('file_search'); +}); + +test('to array', function () { + $response = FileSearchTool::from(toolFileSearch()); + + expect($response->toArray()) + ->toBeArray() + ->toBe(toolFileSearch()); +}); diff --git a/tests/Testing/ClientFakeResponses.php b/tests/Testing/ClientFakeResponses.php new file mode 100644 index 00000000..8db5636f --- /dev/null +++ b/tests/Testing/ClientFakeResponses.php @@ -0,0 +1,158 @@ + 'gpt-4o', + 'tools' => [['type' => 'web_search_preview']], + 'input' => 'what was a positive news story from today?', + ]), + ]); + + $response = $fake->responses()->create([ + 'model' => 'gpt-4o', + 'tools' => [['type' => 'web_search_preview']], + 'input' => 'what was a positive news story from today?', + ]); + + expect($response['model'])->toBe('gpt-4o'); + expect($response['tools'][0]['type'])->toBe('web_search_preview'); +}); + +it('returns a fake response for retrieve', function () { + $fake = new ClientFake([ + RetrieveResponse::fake([ + 'id' => 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', + ]), + ]); + + $response = $fake->responses()->retrieve('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); + + expect($response) + ->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c') + ->object->toBe('response'); +}); + +it('returns a fake response for delete', function () { + $fake = new ClientFake([ + DeleteResponse::fake(), + ]); + + $response = $fake->responses()->delete('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); + + expect($response) + ->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c') + ->deleted->toBeTrue(); +}); + +it('returns a fake response for list', function () { + $fake = new ClientFake([ + ListInputItems::fake(), + ]); + + $response = $fake->responses()->list('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); + + expect($response->data)->toBeArray(); +}); + +it('asserts a create request was sent', function () { + $fake = new ClientFake([ + CreateResponse::fake(), + ]); + + $fake->responses()->create([ + 'model' => 'gpt-4o', + 'tools' => [['type' => 'web_search_preview']], + 'input' => 'what was a positive news story from today?', + ]); + + $fake->assertSent(Responses::class, function ($method, $parameters) { + return $method === 'create' && + $parameters['model'] === 'gpt-4o' && + $parameters['tools'][0]['type'] === 'web_search_preview' && + $parameters['input'] === 'what was a positive news story from today?'; + }); +}); + +it('asserts a retrieve request was sent', function () { + $fake = new ClientFake([ + RetrieveResponse::fake(), + ]); + + $fake->responses()->retrieve('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); + + $fake->assertSent(Responses::class, function ($method, $responseId) { + return $method === 'retrieve' && + $responseId === 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'; + }); +}); + +it('asserts a delete request was sent', function () { + $fake = new ClientFake([ + DeleteResponse::fake(), + ]); + + $fake->responses()->delete('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); + + $fake->assertSent(Responses::class, function ($method, $responseId) { + return $method === 'delete' && + $responseId === 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'; + }); +}); + +it('asserts a list request was sent', function () { + $fake = new ClientFake([ + ListInputItems::fake(), + ]); + + $fake->responses()->list('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); + + $fake->assertSent(Responses::class, function ($method, $responseId) { + return $method === 'list' && + $responseId === 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'; + }); +}); + +it('throws an exception if there are no more fake responses', function () { + $fake = new ClientFake([ + CreateResponse::fake(), + ]); + + $fake->responses()->create([ + 'model' => 'gpt-4o', + 'tools' => [ + [ + 'type' => 'web_search_preview', + ], + ], + 'input' => 'what was a positive news story from today?', + ]); + + $fake->responses()->create([ + 'model' => 'gpt-4o', + 'tools' => [ + [ + 'type' => 'web_search_preview', + ], + ], + 'input' => 'what was a positive news story from today?', + ]); +})->expectExceptionMessage('No fake responses left'); + +it('throws an exception if a request was not sent', function () { + $fake = new ClientFake([ + CreateResponse::fake(), + ]); + + $fake->assertSent(Responses::class, function ($method, $parameters) { + return $method === 'create'; + }); +})->expectException(ExpectationFailedException::class); diff --git a/tests/Testing/Resources/ResponsesTestResource.php b/tests/Testing/Resources/ResponsesTestResource.php new file mode 100644 index 00000000..705f04e7 --- /dev/null +++ b/tests/Testing/Resources/ResponsesTestResource.php @@ -0,0 +1,74 @@ +responses()->create([ + 'model' => 'gpt-4o-mini', + 'tools' => [ + [ + 'type' => 'web_search_preview', + ], + ], + 'input' => 'what was a positive news story from today?', + ]); + + $fake->assertSent(Responses::class, function ($method, $parameters) { + return $method === 'create' && + $parameters['model'] === 'gpt-4o-mini' && + $parameters['tools'] === [ + [ + 'type' => 'web_search_preview', + ], + ] && + $parameters['input'] === 'what was a positive news story from today?'; + }); +}); + +it('records a response retrieve request', function () { + $fake = new ClientFake([ + RetrieveResponse::fake(), + ]); + + $fake->responses()->retrieve('asst_SMzoVX8XmCZEg1EbMHoAm8tc'); + + $fake->assertSent(Responses::class, function ($method, $responseId) { + return $method === 'retrieve' && + $responseId === 'asst_SMzoVX8XmCZEg1EbMHoAm8tc'; + }); +}); + +it('records a response delete request', function () { + $fake = new ClientFake([ + DeleteResponse::fake(), + ]); + + $fake->responses()->delete('asst_SMzoVX8XmCZEg1EbMHoAm8tc'); + + $fake->assertSent(Responses::class, function ($method, $responseId) { + return $method === 'delete' && + $responseId === 'asst_SMzoVX8XmCZEg1EbMHoAm8tc'; + }); +}); + +it('records a response list request', function () { + $fake = new ClientFake([ + ListInputItems::fake(), + ]); + + $fake->responses()->list('asst_SMzoVX8XmCZEg1EbMHoAm8tc'); + + $fake->assertSent(Responses::class, function ($method, $responseId) { + return $method === 'list' && + $responseId === 'asst_SMzoVX8XmCZEg1EbMHoAm8tc'; + }); +}); From 399229860cea244843753bf1d9c28aee0e74c3a6 Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Wed, 14 May 2025 17:43:59 -0400 Subject: [PATCH 04/14] doc: update changelog (v0.11.2 - v0.13.0) (#578) --- CHANGELOG.md | 66 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85e743ff..9277417e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,66 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## v0.13.0 (2025-05-14) +### Added +- Add support for Responses API ([#541](https://github.com/openai-php/client/pull/541)) + +### Fixed +- Add Throwable type support to ClientFake responses ([#576](https://github.com/openai-php/client/pull/576)) + +## v0.12.0 (2025-05-04) +### Changed +- Removed PHP 8.1 support + +## v0.11.0 (2025-05-04) +### Added +- Add logprobs to Chat Response ([#533](https://github.com/openai-php/client/pull/533)) +- Add ResponseHasMetaInformationContract contract to ThreadRunStepResponse ([#523](https://github.com/openai-php/client/pull/523)) +- Add support for 'attributes' on vector store files ([#551](https://github.com/openai-php/client/pull/551)) +- Add OpenAI compatibility support for Google Gemini ([#502](https://github.com/openai-php/client/pull/502)) +- Add compatibility for Aliyun LLM APIs ([#530](https://github.com/openai-php/client/pull/530)) +- Add ability to pass arguments to files list request ([#557](https://github.com/openai-php/client/pull/557)) +- Add search vector store functionality ([#559](https://github.com/openai-php/client/pull/559)) +- Add Image Response usage ([#571](https://github.com/openai-php/client/pull/571)) +- Add category applied input types to moderation response ([#572](https://github.com/openai-php/client/pull/572)) +- Add support for annotations in Chat response (Web Search) ([#564](https://github.com/openai-php/client/pull/564)) +- Add test coverage for assistants streaming and related functionality ([#444](https://github.com/openai-php/client/pull/444)) + +### Changed +- Update GitHub Action deprecations & opt into Dependabot ([#544](https://github.com/openai-php/client/pull/544)) +- Draw attention away from deprecated completions endpoints in docs ([#548](https://github.com/openai-php/client/pull/548)) + +### Fixed +- Fix type definition for responses in ClientFake::addResponses method ([#382](https://github.com/openai-php/client/pull/382)) +- Fix Content retrieval in HttpTransport with custom HttpClient ([#343](https://github.com/openai-php/client/pull/343)) +- Fix correct completion endpoint when logprobs missing ([#550](https://github.com/openai-php/client/pull/550)) +- Fix chat completion choices to allow responses without logprobs field ([#554](https://github.com/openai-php/client/pull/554)) +- Fix support for streaming of non-OpenAI models that return "ping" ([#556](https://github.com/openai-php/client/pull/556)) +- Fix OpenRouter token usage response ([#560](https://github.com/openai-php/client/pull/560)) +- Fix Gemini list models ([#567](https://github.com/openai-php/client/pull/567)) + +## v0.10.3 (2024-11-12) +### Added +- Add http status to ErrorException ([#487](https://github.com/openai-php/client/pull/487)) +- Add `cached_usage` to CreateResponseUsage for Chat ([#494](https://github.com/openai-php/client/pull/494)) +- Add moderation categories (Illicit*) ([#495](https://github.com/openai-php/client/pull/495)) + +### Fixed +- Fix missing parameters on FineTuning RetrieveJobResponse ([#496](https://github.com/openai-php/client/pull/496)) +- Fix nullable description on Assistants Tool Function ([#484](https://github.com/openai-php/client/pull/484)) +- Fix attachment key missing on ThreadMessageResponse ([#471](https://github.com/openai-php/client/pull/471)) + +## v0.10.2 (2024-10-17) +### Added +- Add `thread.run.incomplete` to ThreadRunStreamResponse ([#421](https://github.com/openai-php/client/pull/421)) +- Add `withProject` to configure the project for the client ([#377](https://github.com/openai-php/client/pull/377)) +- Add fake `b64_json` to support mocking ([#462](https://github.com/openai-php/client/pull/462)) + +### Fixed +- Fix image url content type to use `url` instead of `file_id` ([#422](https://github.com/openai-php/client/pull/422)) +- Fix type error on VectorStoresFilesTestResources ([#460](https://github.com/openai-php/client/pull/460)) +- Fix vector store cancel method ([#435](https://github.com/openai-php/client/pull/435)) + ## v0.10.1 (2024-06-06) ### Added - Add support for Assistants API v2 and Vector Stores endpoints ([#420](https://github.com/openai-php/client/pull/420)) @@ -19,8 +79,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - Support for usage stream option on chat endpoint ([#398](https://github.com/openai-php/client/pull/398)) -- ### Fixed -- Missing output paramenter on streamed code interpreter outpu ([#406](https://github.com/openai-php/client/pull/406)) +### Fixed +- Missing output parameter on streamed code interpreter output ([#406](https://github.com/openai-php/client/pull/406)) ## v0.9.1 (2024-05-24) ### Added @@ -212,7 +272,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## v0.4.1 (2023-03-24) ### Added -- Stream suppport ([#84](https://github.com/openai-php/client/pull/84)) +- Stream support ([#84](https://github.com/openai-php/client/pull/84)) ## v0.4.0 (2023-03-17) ### Changed From a0cc3d6019337247743070e773d829d40ce0d991 Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Mon, 19 May 2025 07:19:35 -0400 Subject: [PATCH 05/14] feat(OpenAI) - Add helper property 'output_text' to Responses API (#579) * feat: support 'output_text' on Responses API * fix: support null on 'output_text' --- src/Responses/Responses/CreateResponse.php | 18 +++++++++++++++++- tests/Fixtures/Responses.php | 19 +++++++++++++++++++ tests/Responses/Responses/CreateResponse.php | 18 +++++++++++++++++- 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/Responses/Responses/CreateResponse.php b/src/Responses/Responses/CreateResponse.php index a803dadf..59498964 100644 --- a/src/Responses/Responses/CreateResponse.php +++ b/src/Responses/Responses/CreateResponse.php @@ -13,6 +13,7 @@ use OpenAI\Responses\Responses\Output\OutputFileSearchToolCall; use OpenAI\Responses\Responses\Output\OutputFunctionToolCall; use OpenAI\Responses\Responses\Output\OutputMessage; +use OpenAI\Responses\Responses\Output\OutputMessageContentOutputText; use OpenAI\Responses\Responses\Output\OutputReasoning; use OpenAI\Responses\Responses\Output\OutputWebSearchToolCall; use OpenAI\Responses\Responses\Tool\ComputerUseTool; @@ -45,7 +46,7 @@ * @phpstan-type ToolChoiceType 'none'|'auto'|'required'|FunctionToolChoiceType|HostedToolChoiceType * @phpstan-type ToolsType array * @phpstan-type OutputType array - * @phpstan-type CreateResponseType array{id: string, object: 'response', created_at: int, status: 'completed'|'failed'|'in_progress'|'incomplete', error: ErrorType|null, incomplete_details: IncompleteDetailsType|null, instructions: string|null, max_output_tokens: int|null, model: string, output: OutputType, parallel_tool_calls: bool, previous_response_id: string|null, reasoning: ReasoningType|null, store: bool, temperature: float|null, text: ResponseFormatType, tool_choice: ToolChoiceType, tools: ToolsType, top_p: float|null, truncation: 'auto'|'disabled'|null, usage: UsageType|null, user: string|null, metadata: array|null} + * @phpstan-type CreateResponseType array{id: string, object: 'response', created_at: int, status: 'completed'|'failed'|'in_progress'|'incomplete', error: ErrorType|null, incomplete_details: IncompleteDetailsType|null, instructions: string|null, max_output_tokens: int|null, model: string, output: OutputType, output_text: string|null, parallel_tool_calls: bool, previous_response_id: string|null, reasoning: ReasoningType|null, store: bool, temperature: float|null, text: ResponseFormatType, tool_choice: ToolChoiceType, tools: ToolsType, top_p: float|null, truncation: 'auto'|'disabled'|null, usage: UsageType|null, user: string|null, metadata: array|null} * * @implements ResponseContract */ @@ -78,6 +79,7 @@ private function __construct( public readonly ?int $maxOutputTokens, public readonly string $model, public readonly array $output, + public readonly ?string $outputText, public readonly bool $parallelToolCalls, public readonly ?string $previousResponseId, public readonly ?CreateResponseReasoning $reasoning, @@ -128,6 +130,18 @@ public static function from(array $attributes, MetaInformation $meta): self $attributes['tools'], ); + // Remake the sdk only property output_text. + $texts = []; + foreach ($output as $item) { + if ($item instanceof OutputMessage) { + foreach ($item->content as $content) { + if ($content instanceof OutputMessageContentOutputText) { + $texts[] = $content->text; + } + } + } + } + return new self( id: $attributes['id'], object: $attributes['object'], @@ -143,6 +157,7 @@ public static function from(array $attributes, MetaInformation $meta): self maxOutputTokens: $attributes['max_output_tokens'], model: $attributes['model'], output: $output, + outputText: empty($texts) ? null : implode(' ', $texts), parallelToolCalls: $attributes['parallel_tool_calls'], previousResponseId: $attributes['previous_response_id'], reasoning: isset($attributes['reasoning']) @@ -203,6 +218,7 @@ public function toArray(): array 'truncation' => $this->truncation, 'usage' => $this->usage?->toArray(), 'user' => $this->user, + 'output_text' => $this->outputText, ]; } } diff --git a/tests/Fixtures/Responses.php b/tests/Fixtures/Responses.php index d9f0b0cd..8a904ae9 100644 --- a/tests/Fixtures/Responses.php +++ b/tests/Fixtures/Responses.php @@ -290,6 +290,25 @@ function outputMessage(): array ]; } +/** + * @return array + */ +function outputMessageOnlyRefusal(): array +{ + return [ + 'content' => [ + [ + 'refusal' => 'The assistant refused to answer.', + 'type' => 'refusal', + ], + ], + 'id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'role' => 'assistant', + 'status' => 'completed', + 'type' => 'message', + ]; +} + /** * @return array */ diff --git a/tests/Responses/Responses/CreateResponse.php b/tests/Responses/Responses/CreateResponse.php index e09e875a..121696f2 100644 --- a/tests/Responses/Responses/CreateResponse.php +++ b/tests/Responses/Responses/CreateResponse.php @@ -48,9 +48,25 @@ test('to array', function () { $response = CreateResponse::from(createResponseResource(), meta()); + $expected = createResponseResource(); + $expected['output_text'] = 'As of today, March 9, 2025, one notable positive news story...'; + + expect($response->toArray()) + ->toBeArray() + ->toBe($expected); +}); + +test('to array with no messages', function () { + $payload = createResponseResource(); + $payload['output'] = [ + outputMessageOnlyRefusal(), + ]; + + $response = CreateResponse::from($payload, meta()); + expect($response->toArray()) ->toBeArray() - ->toBe(createResponseResource()); + ->outputText->toBeNull(); }); test('fake', function () { From 199d23715ae9568307bbc2d68e24787cb78b3cca Mon Sep 17 00:00:00 2001 From: clem Date: Thu, 22 May 2025 16:25:03 +0200 Subject: [PATCH 06/14] fix(OpenAI) - Add index to CreateStreamedResponseToolCall (#562) * Update CreateStreamedResponseToolCall.php * provide index to CreateStreamedResponseToolCall toArray method * fix type coverage in CreateStreamedResponseToolCall toArray method --- src/Responses/Chat/CreateStreamedResponseToolCall.php | 9 ++++++--- tests/Fixtures/Chat.php | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Responses/Chat/CreateStreamedResponseToolCall.php b/src/Responses/Chat/CreateStreamedResponseToolCall.php index 2c206b2a..9173c653 100644 --- a/src/Responses/Chat/CreateStreamedResponseToolCall.php +++ b/src/Responses/Chat/CreateStreamedResponseToolCall.php @@ -7,17 +7,19 @@ final class CreateStreamedResponseToolCall { private function __construct( + public readonly ?int $index, public readonly ?string $id, public readonly ?string $type, public readonly CreateStreamedResponseToolCallFunction $function, ) {} /** - * @param array{id?: string, type?: string, function: array{name?: string, arguments: string}} $attributes + * @param array{index?: int, id?: string, type?: string, function: array{name?: string, arguments: string}} $attributes */ public static function from(array $attributes): self { return new self( + $attributes['index'] ?? null, $attributes['id'] ?? null, $attributes['type'] ?? null, CreateStreamedResponseToolCallFunction::from($attributes['function']), @@ -25,14 +27,15 @@ public static function from(array $attributes): self } /** - * @return array{id?: string, type?: string, function?: array{name?: string, arguments: string}} + * @return array{index?: int, id?: string, type?: string, function?: array{name?: string, arguments: string}} */ public function toArray(): array { return array_filter([ + 'index' => $this->index, 'id' => $this->id, 'type' => $this->type, 'function' => $this->function->toArray(), - ]); + ], fn (mixed $value): bool => ! is_null($value)); } } diff --git a/tests/Fixtures/Chat.php b/tests/Fixtures/Chat.php index 94aed2ca..89236010 100644 --- a/tests/Fixtures/Chat.php +++ b/tests/Fixtures/Chat.php @@ -601,6 +601,7 @@ function chatCompletionStreamToolCallsChunk(): array 'delta' => [ 'tool_calls' => [ [ + 'index' => 0, 'id' => 'call_trlgKnhMpYSC7CFXKw3CceUZ', 'type' => 'function', 'function' => [ From 68fd99c49acded4baec900613631a9a4867dd8e7 Mon Sep 17 00:00:00 2001 From: Erdi Arikan <88897583+erdiarikan@users.noreply.github.com> Date: Wed, 28 May 2025 01:33:47 +0100 Subject: [PATCH 07/14] fix(OpenAI): $parameters array is not required in thread 'create' method (#577) * feat: $parameters array is not required in thread 'create' method * fix: default to empty array on params * test: assert no params needed on thread.create --------- Co-authored-by: Connor Tumbleson --- src/Resources/Threads.php | 2 +- tests/Resources/Threads.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Resources/Threads.php b/src/Resources/Threads.php index 8974bbdb..18315e3c 100644 --- a/src/Resources/Threads.php +++ b/src/Resources/Threads.php @@ -27,7 +27,7 @@ final class Threads implements ThreadsContract * * @param array $parameters */ - public function create(array $parameters): ThreadResponse + public function create(array $parameters = []): ThreadResponse { $payload = Payload::create('threads', $parameters); diff --git a/tests/Resources/Threads.php b/tests/Resources/Threads.php index c1ddedca..025ea7c1 100644 --- a/tests/Resources/Threads.php +++ b/tests/Resources/Threads.php @@ -10,7 +10,7 @@ test('create', function () { $client = mockClient('POST', 'threads', [], Response::from(threadResource(), metaHeaders())); - $result = $client->threads()->create([]); + $result = $client->threads()->create(); expect($result) ->toBeInstanceOf(ThreadResponse::class) From 59e27ca6008ab8b8bd0430699732fcf56b309dfd Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Tue, 27 May 2025 21:23:22 -0400 Subject: [PATCH 08/14] feat(OpenAI): add support for response cancel (#588) --- README.md | 13 +++++++++++++ src/Resources/Responses.php | 15 +++++++++++++++ tests/Resources/Responses.php | 17 +++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/README.md b/README.md index 25fd7467..609f9f53 100644 --- a/README.md +++ b/README.md @@ -254,6 +254,19 @@ $response->truncation; // 'disabled' $response->toArray(); // ['id' => 'resp_67ccd2bed1ec8190b14f964abc054267', ...] ``` +### `cancel` + +Cancel a model response (background request) with the given ID. + +```php +$response = $client->responses()->cancel('resp_67ccd2bed1ec8190b14f964abc054267'); + +$response->id; // 'resp_67ccd2bed1ec8190b14f964abc054267' +$response->status; // 'canceled' + +$response->toArray(); // ['id' => 'resp_67ccd2bed1ec8190b14f964abc054267', 'status' => 'canceled', ...] +``` + ### `delete` Deletes a model response with the given ID. diff --git a/src/Resources/Responses.php b/src/Resources/Responses.php index c31357b3..ffa4bcec 100644 --- a/src/Resources/Responses.php +++ b/src/Resources/Responses.php @@ -80,6 +80,21 @@ public function retrieve(string $id): RetrieveResponse return RetrieveResponse::from($response->data(), $response->meta()); } + /** + * Cancels a model response with the given ID. Must be marked as 'background' to be cancellable. + * + * @see https://platform.openai.com/docs/api-reference/responses/cancel + */ + public function cancel(string $id): RetrieveResponse + { + $payload = Payload::cancel('responses', $id); + + /** @var Response $response */ + $response = $this->transporter->requestObject($payload); + + return RetrieveResponse::from($response->data(), $response->meta()); + } + /** * Deletes a model response with the given ID. * diff --git a/tests/Resources/Responses.php b/tests/Resources/Responses.php index 466114f8..b589c364 100644 --- a/tests/Resources/Responses.php +++ b/tests/Resources/Responses.php @@ -201,3 +201,20 @@ expect($result->meta()) ->toBeInstanceOf(MetaInformation::class); }); + +test('cancel', function () { + $client = mockClient('POST', 'responses/resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c/cancel', [ + ], \OpenAI\ValueObjects\Transporter\Response::from(retrieveResponseResource(), metaHeaders())); + + $result = $client->responses()->cancel('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); + + expect($result) + ->toBeInstanceOf(RetrieveResponse::class) + ->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c') + ->object->toBe('response') + ->createdAt->toBe(1741484430) + ->status->toBe('completed'); + + expect($result->meta()) + ->toBeInstanceOf(MetaInformation::class); +}); From 0e66f10b1ab16ba322efa837347fe32c8d631096 Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Mon, 2 Jun 2025 15:38:20 -0400 Subject: [PATCH 09/14] fix(OpenAI): add testing for response cancel (#592) --- src/Contracts/Resources/ResponsesContract.php | 7 ++ .../Resources/ResponsesTestResource.php | 5 + .../Responses/CancelResponseFixture.php | 103 ++++++++++++++++++ .../Resources/ResponsesTestResource.php | 13 +++ 4 files changed, 128 insertions(+) create mode 100644 src/Testing/Responses/Fixtures/Responses/CancelResponseFixture.php diff --git a/src/Contracts/Resources/ResponsesContract.php b/src/Contracts/Resources/ResponsesContract.php index 4d13d6ed..4fd76a21 100644 --- a/src/Contracts/Resources/ResponsesContract.php +++ b/src/Contracts/Resources/ResponsesContract.php @@ -42,6 +42,13 @@ public function createStreamed(array $parameters): StreamResponse; */ public function retrieve(string $id): RetrieveResponse; + /** + * Cancels a model response with the given ID. Must be marked as 'background' to be cancellable. + * + * @see https://platform.openai.com/docs/api-reference/responses/cancel + */ + public function cancel(string $id): RetrieveResponse; + /** * Deletes a model response with the given ID. * diff --git a/src/Testing/Resources/ResponsesTestResource.php b/src/Testing/Resources/ResponsesTestResource.php index 7468b3a8..14470222 100644 --- a/src/Testing/Resources/ResponsesTestResource.php +++ b/src/Testing/Resources/ResponsesTestResource.php @@ -40,6 +40,11 @@ public function list(string $id, array $parameters = []): ListInputItems return $this->record(__FUNCTION__, func_get_args()); } + public function cancel(string $id): RetrieveResponse + { + return $this->record(__FUNCTION__, func_get_args()); + } + public function delete(string $id): DeleteResponse { return $this->record(__FUNCTION__, func_get_args()); diff --git a/src/Testing/Responses/Fixtures/Responses/CancelResponseFixture.php b/src/Testing/Responses/Fixtures/Responses/CancelResponseFixture.php new file mode 100644 index 00000000..f3058291 --- /dev/null +++ b/src/Testing/Responses/Fixtures/Responses/CancelResponseFixture.php @@ -0,0 +1,103 @@ + 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', + 'object' => 'response', + 'created_at' => 1741484430, + 'status' => 'completed', + 'error' => null, + 'incomplete_details' => null, + 'instructions' => null, + 'max_output_tokens' => null, + 'model' => 'gpt-4o-2024-08-06', + 'output' => [ + [ + 'type' => 'web_search_call', + 'id' => 'ws_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'status' => 'completed', + ], + [ + 'type' => 'message', + 'id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'status' => 'completed', + 'role' => 'assistant', + 'content' => [ + [ + 'type' => 'output_text', + 'text' => 'As of today, March 9, 2025, one notable positive news story...', + 'annotations' => [ + [ + 'type' => 'url_citation', + 'start_index' => 442, + 'end_index' => 557, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + [ + 'type' => 'url_citation', + 'start_index' => 962, + 'end_index' => 1077, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + [ + 'type' => 'url_citation', + 'start_index' => 1336, + 'end_index' => 1451, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + ], + ], + ], + ], + ], + 'parallel_tool_calls' => true, + 'previous_response_id' => null, + 'reasoning' => [ + 'effort' => null, + 'generate_summary' => null, + ], + 'store' => true, + 'temperature' => 1.0, + 'text' => [ + 'format' => [ + 'type' => 'text', + ], + ], + 'tool_choice' => 'auto', + 'tools' => [ + [ + 'type' => 'web_search_preview', + 'domains' => [], + 'search_context_size' => 'medium', + 'user_location' => [ + 'type' => 'approximate', + 'city' => 'San Francisco', + 'country' => 'US', + 'region' => 'California', + 'timezone' => 'America/Los_Angeles', + ], + ], + ], + 'top_p' => 1.0, + 'truncation' => 'disabled', + 'usage' => [ + 'input_tokens' => 328, + 'input_tokens_details' => [ + 'cached_tokens' => 0, + ], + 'output_tokens' => 356, + 'output_tokens_details' => [ + 'reasoning_tokens' => 0, + ], + 'total_tokens' => 684, + ], + 'user' => null, + 'metadata' => [], + ]; +} diff --git a/tests/Testing/Resources/ResponsesTestResource.php b/tests/Testing/Resources/ResponsesTestResource.php index 705f04e7..d78529b0 100644 --- a/tests/Testing/Resources/ResponsesTestResource.php +++ b/tests/Testing/Resources/ResponsesTestResource.php @@ -47,6 +47,19 @@ }); }); +it('records a response cancel request', function () { + $fake = new ClientFake([ + RetrieveResponse::fake(), + ]); + + $fake->responses()->cancel('asst_SMzoVX8XmCZEg1EbMHoAm8tc'); + + $fake->assertSent(Responses::class, function ($method, $responseId) { + return $method === 'cancel' && + $responseId === 'asst_SMzoVX8XmCZEg1EbMHoAm8tc'; + }); +}); + it('records a response delete request', function () { $fake = new ClientFake([ DeleteResponse::fake(), From 4f956243697ddf5059ce9ed902ddca7fbd1e6182 Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Tue, 3 Jun 2025 08:00:34 -0400 Subject: [PATCH 10/14] feat(OpenAI) - Add support for Image Generation Tool (Responses) (#594) --- src/Responses/Responses/CreateResponse.php | 11 ++- src/Responses/Responses/RetrieveResponse.php | 11 ++- .../Tool/ImageGenerationInputImageMask.php | 51 +++++++++++ .../Responses/Tool/ImageGenerationTool.php | 86 +++++++++++++++++++ tests/Fixtures/Responses.php | 21 +++++ .../Responses/Responses/RetrieveResponse.php | 2 +- .../Responses/Tool/ImageGenerationTool.php | 56 ++++++++++++ 7 files changed, 229 insertions(+), 9 deletions(-) create mode 100644 src/Responses/Responses/Tool/ImageGenerationInputImageMask.php create mode 100644 src/Responses/Responses/Tool/ImageGenerationTool.php create mode 100644 tests/Responses/Responses/Tool/ImageGenerationTool.php diff --git a/src/Responses/Responses/CreateResponse.php b/src/Responses/Responses/CreateResponse.php index 59498964..2e6caa04 100644 --- a/src/Responses/Responses/CreateResponse.php +++ b/src/Responses/Responses/CreateResponse.php @@ -19,6 +19,7 @@ use OpenAI\Responses\Responses\Tool\ComputerUseTool; use OpenAI\Responses\Responses\Tool\FileSearchTool; use OpenAI\Responses\Responses\Tool\FunctionTool; +use OpenAI\Responses\Responses\Tool\ImageGenerationTool; use OpenAI\Responses\Responses\Tool\WebSearchTool; use OpenAI\Responses\Responses\ToolChoice\FunctionToolChoice; use OpenAI\Responses\Responses\ToolChoice\HostedToolChoice; @@ -34,6 +35,7 @@ * @phpstan-import-type OutputWebSearchToolCallType from OutputWebSearchToolCall * @phpstan-import-type ComputerUseToolType from ComputerUseTool * @phpstan-import-type FileSearchToolType from FileSearchTool + * @phpstan-import-type ImageGenerationToolType from ImageGenerationTool * @phpstan-import-type FunctionToolType from FunctionTool * @phpstan-import-type WebSearchToolType from WebSearchTool * @phpstan-import-type ErrorType from CreateResponseError @@ -44,7 +46,7 @@ * @phpstan-import-type ReasoningType from CreateResponseReasoning * * @phpstan-type ToolChoiceType 'none'|'auto'|'required'|FunctionToolChoiceType|HostedToolChoiceType - * @phpstan-type ToolsType array + * @phpstan-type ToolsType array * @phpstan-type OutputType array * @phpstan-type CreateResponseType array{id: string, object: 'response', created_at: int, status: 'completed'|'failed'|'in_progress'|'incomplete', error: ErrorType|null, incomplete_details: IncompleteDetailsType|null, instructions: string|null, max_output_tokens: int|null, model: string, output: OutputType, output_text: string|null, parallel_tool_calls: bool, previous_response_id: string|null, reasoning: ReasoningType|null, store: bool, temperature: float|null, text: ResponseFormatType, tool_choice: ToolChoiceType, tools: ToolsType, top_p: float|null, truncation: 'auto'|'disabled'|null, usage: UsageType|null, user: string|null, metadata: array|null} * @@ -64,7 +66,7 @@ final class CreateResponse implements ResponseContract, ResponseHasMetaInformati * @param 'response' $object * @param 'completed'|'failed'|'in_progress'|'incomplete' $status * @param array $output - * @param array $tools + * @param array $tools * @param 'auto'|'disabled'|null $truncation * @param array $metadata */ @@ -121,11 +123,12 @@ public static function from(array $attributes, MetaInformation $meta): self : $attributes['tool_choice']; $tools = array_map( - fn (array $tool): ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool => match ($tool['type']) { + fn (array $tool): ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool => match ($tool['type']) { 'file_search' => FileSearchTool::from($tool), 'web_search_preview', 'web_search_preview_2025_03_11' => WebSearchTool::from($tool), 'function' => FunctionTool::from($tool), 'computer_use_preview' => ComputerUseTool::from($tool), + 'image_generation' => ImageGenerationTool::from($tool), }, $attributes['tools'], ); @@ -211,7 +214,7 @@ public function toArray(): array ? $this->toolChoice : $this->toolChoice->toArray(), 'tools' => array_map( - fn (ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool $tool): array => $tool->toArray(), + fn (ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool $tool): array => $tool->toArray(), $this->tools ), 'top_p' => $this->topP, diff --git a/src/Responses/Responses/RetrieveResponse.php b/src/Responses/Responses/RetrieveResponse.php index 35902b80..1005da25 100644 --- a/src/Responses/Responses/RetrieveResponse.php +++ b/src/Responses/Responses/RetrieveResponse.php @@ -18,6 +18,7 @@ use OpenAI\Responses\Responses\Tool\ComputerUseTool; use OpenAI\Responses\Responses\Tool\FileSearchTool; use OpenAI\Responses\Responses\Tool\FunctionTool; +use OpenAI\Responses\Responses\Tool\ImageGenerationTool; use OpenAI\Responses\Responses\Tool\WebSearchTool; use OpenAI\Responses\Responses\ToolChoice\FunctionToolChoice; use OpenAI\Responses\Responses\ToolChoice\HostedToolChoice; @@ -33,6 +34,7 @@ * @phpstan-import-type OutputWebSearchToolCallType from OutputWebSearchToolCall * @phpstan-import-type ComputerUseToolType from ComputerUseTool * @phpstan-import-type FileSearchToolType from FileSearchTool + * @phpstan-import-type ImageGenerationToolType from ImageGenerationTool * @phpstan-import-type FunctionToolType from FunctionTool * @phpstan-import-type WebSearchToolType from WebSearchTool * @phpstan-import-type ErrorType from CreateResponseError @@ -43,7 +45,7 @@ * @phpstan-import-type ReasoningType from CreateResponseReasoning * * @phpstan-type ToolChoiceType 'none'|'auto'|'required'|FunctionToolChoiceType|HostedToolChoiceType - * @phpstan-type ToolsType array + * @phpstan-type ToolsType array * @phpstan-type OutputType array * @phpstan-type RetrieveResponseType array{id: string, object: 'response', created_at: int, status: 'completed'|'failed'|'in_progress'|'incomplete', error: ErrorType|null, incomplete_details: IncompleteDetailsType|null, instructions: string|null, max_output_tokens: int|null, model: string, output: OutputType, parallel_tool_calls: bool, previous_response_id: string|null, reasoning: ReasoningType|null, store: bool, temperature: float|null, text: ResponseFormatType, tool_choice: ToolChoiceType, tools: ToolsType, top_p: float|null, truncation: 'auto'|'disabled'|null, usage: UsageType|null, user: string|null, metadata: array|null} * @@ -63,7 +65,7 @@ final class RetrieveResponse implements ResponseContract, ResponseHasMetaInforma * @param 'response' $object * @param 'completed'|'failed'|'in_progress'|'incomplete' $status * @param array $output - * @param array $tools + * @param array $tools * @param 'auto'|'disabled'|null $truncation * @param array $metadata */ @@ -119,11 +121,12 @@ public static function from(array $attributes, MetaInformation $meta): self : $attributes['tool_choice']; $tools = array_map( - fn (array $tool): ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool => match ($tool['type']) { + fn (array $tool): ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool => match ($tool['type']) { 'file_search' => FileSearchTool::from($tool), 'web_search_preview', 'web_search_preview_2025_03_11' => WebSearchTool::from($tool), 'function' => FunctionTool::from($tool), 'computer_use_preview' => ComputerUseTool::from($tool), + 'image_generation' => ImageGenerationTool::from($tool), }, $attributes['tools'], ); @@ -196,7 +199,7 @@ public function toArray(): array ? $this->toolChoice : $this->toolChoice->toArray(), 'tools' => array_map( - fn (ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool $tool): array => $tool->toArray(), + fn (ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool $tool): array => $tool->toArray(), $this->tools ), 'top_p' => $this->topP, diff --git a/src/Responses/Responses/Tool/ImageGenerationInputImageMask.php b/src/Responses/Responses/Tool/ImageGenerationInputImageMask.php new file mode 100644 index 00000000..3dfd46e4 --- /dev/null +++ b/src/Responses/Responses/Tool/ImageGenerationInputImageMask.php @@ -0,0 +1,51 @@ + + */ +final class ImageGenerationInputImageMask implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly ?string $fileId, + public readonly ?string $imageUrl, + ) {} + + /** + * @param InputImageMaskType $attributes + */ + public static function from(array $attributes): self + { + return new self( + fileId: $attributes['file_id'] ?? null, + imageUrl: $attributes['image_url'] ?? null, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'file_id' => $this->fileId, + 'image_url' => $this->imageUrl, + ]; + } +} diff --git a/src/Responses/Responses/Tool/ImageGenerationTool.php b/src/Responses/Responses/Tool/ImageGenerationTool.php new file mode 100644 index 00000000..a703e0f6 --- /dev/null +++ b/src/Responses/Responses/Tool/ImageGenerationTool.php @@ -0,0 +1,86 @@ + + */ +final class ImageGenerationTool implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'image_generation' $type + * @param 'transparent'|'opaque'|'auto' $background + * @param 'jpeg'|'png'|'webp' $outputFormat + * @param 'low'|'medium'|'high'|'auto' $quality + * @param "1024x1024"|"1024x1536"|"1536x1024"|'auto' $size + */ + private function __construct( + public readonly string $type, + public readonly string $background, + public readonly ?ImageGenerationInputImageMask $inputImageMask, + public readonly string $model, + public readonly string $moderation, + public readonly int $outputCompression, + public readonly string $outputFormat, + public readonly int $partialImages, + public readonly string $quality, + public readonly string $size, + ) {} + + /** + * @param ImageGenerationToolType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + background: $attributes['background'], + inputImageMask: isset($attributes['input_image_mask']) + ? ImageGenerationInputImageMask::from($attributes['input_image_mask']) + : null, + model: $attributes['model'], + moderation: $attributes['moderation'], + outputCompression: $attributes['output_compression'], + outputFormat: $attributes['output_format'], + partialImages: $attributes['partial_images'], + quality: $attributes['quality'], + size: $attributes['size'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'background' => $this->background, + 'input_image_mask' => $this->inputImageMask?->toArray(), + 'model' => $this->model, + 'moderation' => $this->moderation, + 'output_compression' => $this->outputCompression, + 'output_format' => $this->outputFormat, + 'partial_images' => $this->partialImages, + 'quality' => $this->quality, + 'size' => $this->size, + ]; + } +} diff --git a/tests/Fixtures/Responses.php b/tests/Fixtures/Responses.php index 8a904ae9..562ff184 100644 --- a/tests/Fixtures/Responses.php +++ b/tests/Fixtures/Responses.php @@ -40,6 +40,7 @@ function createResponseResource(): array 'tools' => [ toolWebSearchPreview(), toolFileSearch(), + toolImageGeneration(), ], 'top_p' => 1.0, 'truncation' => 'disabled', @@ -95,6 +96,7 @@ function retrieveResponseResource(): array 'tools' => [ toolWebSearchPreview(), toolFileSearch(), + toolImageGeneration(), ], 'top_p' => 1.0, 'truncation' => 'disabled', @@ -327,6 +329,25 @@ function toolWebSearchPreview(): array ]; } +/** + * @return array + */ +function toolImageGeneration(): array +{ + return [ + 'type' => 'image_generation', + 'background' => 'transparent', + 'input_image_mask' => null, + 'model' => 'gpt-image-1', + 'moderation' => 'auto', + 'output_compression' => 100, + 'output_format' => 'png', + 'partial_images' => 0, + 'quality' => 'auto', + 'size' => 'auto', + ]; +} + /** * @return array */ diff --git a/tests/Responses/Responses/RetrieveResponse.php b/tests/Responses/Responses/RetrieveResponse.php index e720562f..2a19220a 100644 --- a/tests/Responses/Responses/RetrieveResponse.php +++ b/tests/Responses/Responses/RetrieveResponse.php @@ -30,7 +30,7 @@ ->text->toBeInstanceOf(CreateResponseFormat::class) ->toolChoice->toBe('auto') ->tools->toBeArray() - ->tools->toHaveCount(2) + ->tools->toHaveCount(3) ->topP->toBe(1.0) ->truncation->toBe('disabled') ->usage->toBeInstanceOf(CreateResponseUsage::class) diff --git a/tests/Responses/Responses/Tool/ImageGenerationTool.php b/tests/Responses/Responses/Tool/ImageGenerationTool.php new file mode 100644 index 00000000..2f49f12d --- /dev/null +++ b/tests/Responses/Responses/Tool/ImageGenerationTool.php @@ -0,0 +1,56 @@ +toBeInstanceOf(ImageGenerationTool::class) + ->type->toBe('image_generation') + ->background->toBe('transparent') + ->inputImageMask->toBeNull() + ->model->toBe('gpt-image-1') + ->moderation->toBe('auto') + ->outputCompression->toBe(100) + ->outputFormat->toBe('png') + ->partialImages->toBe(0) + ->quality->toBe('auto') + ->size->toBe('auto'); +}); + +test('from non-null input_image_mask', function () { + $payload = toolImageGeneration(); + $payload['input_image_mask'] = [ + 'image_url' => 'https://example.com/mask.png', + 'file_id' => 'file_1234567890abcdef', + ]; + $response = ImageGenerationTool::from($payload); + + expect($response) + ->toBeInstanceOf(ImageGenerationTool::class) + ->inputImageMask->toBeInstanceOf(ImageGenerationInputImageMask::class); +}); + +test('from results', function () { + $response = ImageGenerationTool::from(toolImageGeneration()); + + expect($response) + ->toBeInstanceOf(ImageGenerationTool::class) + ->type->toBe('image_generation'); +}); + +test('as array accessible', function () { + $response = ImageGenerationTool::from(toolImageGeneration()); + + expect($response['type'])->toBe('image_generation'); +}); + +test('to array', function () { + $response = ImageGenerationTool::from(toolImageGeneration()); + + expect($response->toArray()) + ->toBeArray() + ->toBe(toolImageGeneration()); +}); From 69037e9d3feba865936be5a8bdd016a9d0b6a9d1 Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Tue, 3 Jun 2025 08:30:38 -0400 Subject: [PATCH 11/14] test(OpenAI): augment testing for fake() on Responses API (#593) --- src/Testing/Responses/Concerns/Fakeable.php | 6 ++--- tests/Fixtures/Responses.php | 26 +++++++++++++++++--- tests/Responses/Responses/CreateResponse.php | 10 +++++++- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/Testing/Responses/Concerns/Fakeable.php b/src/Testing/Responses/Concerns/Fakeable.php index ca55e3ef..1ea2f53b 100644 --- a/src/Testing/Responses/Concerns/Fakeable.php +++ b/src/Testing/Responses/Concerns/Fakeable.php @@ -29,9 +29,9 @@ private static function buildAttributes(array $original, array $override): array $new = []; foreach ($original as $key => $entry) { - $new[$key] = is_array($entry) ? - self::buildAttributes($entry, $override[$key] ?? []) : - $override[$key] ?? $entry; + $new[$key] = is_array($entry) + ? self::buildAttributes($entry, $override[$key] ?? []) + : $override[$key] ?? $entry; unset($override[$key]); } diff --git a/tests/Fixtures/Responses.php b/tests/Fixtures/Responses.php index 562ff184..83802bd0 100644 --- a/tests/Fixtures/Responses.php +++ b/tests/Fixtures/Responses.php @@ -17,7 +17,7 @@ function createResponseResource(): array 'metadata' => [], 'model' => 'gpt-4o-2024-08-06', 'output' => [ - outputMessage(), + outputAnnotationMessage(), outputWebSearchToolCall(), outputFileSearchToolCall(), outputComputerToolCall(), @@ -77,7 +77,7 @@ function retrieveResponseResource(): array 'model' => 'gpt-4o-2024-08-06', 'output' => [ outputWebSearchToolCall(), - outputMessage(), + outputAnnotationMessage(), ], 'parallel_tool_calls' => true, 'previous_response_id' => null, @@ -249,7 +249,27 @@ function outputReasoning(): array /** * @return array */ -function outputMessage(): array +function outputBasicMessage(): array +{ + return [ + 'content' => [ + [ + 'annotations' => [], + 'text' => 'This is a basic message.', + 'type' => 'output_text', + ], + ], + 'id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'role' => 'assistant', + 'status' => 'completed', + 'type' => 'message', + ]; +} + +/** + * @return array + */ +function outputAnnotationMessage(): array { return [ 'content' => [ diff --git a/tests/Responses/Responses/CreateResponse.php b/tests/Responses/Responses/CreateResponse.php index 121696f2..4e52f82a 100644 --- a/tests/Responses/Responses/CreateResponse.php +++ b/tests/Responses/Responses/CreateResponse.php @@ -81,10 +81,18 @@ 'id' => 'resp_1234', 'object' => 'custom_response', 'status' => 'failed', + 'output' => [ + outputBasicMessage(), + ], ]); expect($response) ->id->toBe('resp_1234') ->object->toBe('custom_response') - ->status->toBe('failed'); + ->status->toBe('failed') + ->output->toBeArray(); + + expect($response->output[0]['content'][0]) + ->type->toBe('output_text') + ->text->toBe('This is a basic message.'); }); From a2cc4ebc7284ee2b8a2f24cb73a31bd823f547d3 Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Sun, 8 Jun 2025 11:52:21 -0400 Subject: [PATCH 12/14] feat(OpenAI): Realtime Ephermal Tokens (#591) * feat: initial work on realtime tokens * feat: more work on realtime tokens * fix: add realtime class to client * fix: threshold is float, not int * fix: modalities can be null on transcribe * feat: test build out for realtime * chore: correct docblock * test: integration tests for realtime --- src/Client.php | 12 ++ src/Contracts/ClientContract.php | 8 ++ src/Contracts/Resources/RealtimeContract.php | 29 +++++ src/Resources/Realtime.php | 54 +++++++++ .../Realtime/Session/ClientSecret.php | 51 ++++++++ .../Session/InputAudioTranscription.php | 51 ++++++++ .../Realtime/Session/TurnDetection.php | 60 ++++++++++ src/Responses/Realtime/SessionResponse.php | 113 ++++++++++++++++++ src/Responses/Realtime/Tools/FunctionTool.php | 61 ++++++++++ .../InputAudioTranscription.php | 57 +++++++++ .../Realtime/TranscriptionSessionResponse.php | 77 ++++++++++++ src/Testing/ClientFake.php | 6 + .../Resources/RealtimeTestResource.php | 29 +++++ .../Realtime/SessionResponseFixture.php | 32 +++++ .../TranscriptionSessionResponseFixture.php | 22 ++++ tests/Fixtures/Realtime.php | 55 +++++++++ tests/Resources/Realtime.php | 24 ++++ tests/Responses/Realtime/SessionResponse.php | 24 ++++ .../Realtime/TranscriptionSessionResponse.php | 17 +++ .../Resources/RealtimeTestResource.php | 30 +++++ 20 files changed, 812 insertions(+) create mode 100644 src/Contracts/Resources/RealtimeContract.php create mode 100644 src/Resources/Realtime.php create mode 100644 src/Responses/Realtime/Session/ClientSecret.php create mode 100644 src/Responses/Realtime/Session/InputAudioTranscription.php create mode 100644 src/Responses/Realtime/Session/TurnDetection.php create mode 100644 src/Responses/Realtime/SessionResponse.php create mode 100644 src/Responses/Realtime/Tools/FunctionTool.php create mode 100644 src/Responses/Realtime/TranscriptionSession/InputAudioTranscription.php create mode 100644 src/Responses/Realtime/TranscriptionSessionResponse.php create mode 100644 src/Testing/Resources/RealtimeTestResource.php create mode 100644 src/Testing/Responses/Fixtures/Realtime/SessionResponseFixture.php create mode 100644 src/Testing/Responses/Fixtures/Realtime/TranscriptionSessionResponseFixture.php create mode 100644 tests/Fixtures/Realtime.php create mode 100644 tests/Resources/Realtime.php create mode 100644 tests/Responses/Realtime/SessionResponse.php create mode 100644 tests/Responses/Realtime/TranscriptionSessionResponse.php create mode 100644 tests/Testing/Resources/RealtimeTestResource.php diff --git a/src/Client.php b/src/Client.php index 3aca764f..fb019c2e 100644 --- a/src/Client.php +++ b/src/Client.php @@ -5,6 +5,7 @@ namespace OpenAI; use OpenAI\Contracts\ClientContract; +use OpenAI\Contracts\Resources\RealtimeContract; use OpenAI\Contracts\Resources\ThreadsContract; use OpenAI\Contracts\Resources\VectorStoresContract; use OpenAI\Contracts\TransporterContract; @@ -21,6 +22,7 @@ use OpenAI\Resources\Images; use OpenAI\Resources\Models; use OpenAI\Resources\Moderations; +use OpenAI\Resources\Realtime; use OpenAI\Resources\Responses; use OpenAI\Resources\Threads; use OpenAI\Resources\VectorStores; @@ -168,6 +170,16 @@ public function assistants(): Assistants return new Assistants($this->transporter); } + /** + * Communicate with a model in real time using WebRTC or WebSockets. + * + * @see https://platform.openai.com/docs/api-reference/realtime + */ + public function realtime(): RealtimeContract + { + return new Realtime($this->transporter); + } + /** * Create threads that assistants can interact with. * diff --git a/src/Contracts/ClientContract.php b/src/Contracts/ClientContract.php index daf44729..f67c7e36 100644 --- a/src/Contracts/ClientContract.php +++ b/src/Contracts/ClientContract.php @@ -15,6 +15,7 @@ use OpenAI\Contracts\Resources\ImagesContract; use OpenAI\Contracts\Resources\ModelsContract; use OpenAI\Contracts\Resources\ModerationsContract; +use OpenAI\Contracts\Resources\RealtimeContract; use OpenAI\Contracts\Resources\ResponsesContract; use OpenAI\Contracts\Resources\ThreadsContract; use OpenAI\Contracts\Resources\VectorStoresContract; @@ -36,6 +37,13 @@ public function completions(): CompletionsContract; */ public function responses(): ResponsesContract; + /** + * Communicate with a GPT-4o class model in real time using WebRTC or WebSockets. Supports text and audio inputs and outputs, along with audio transcriptions. + * + * @see https://platform.openai.com/docs/api-reference/realtime-sessions + */ + public function realtime(): RealtimeContract; + /** * Given a chat conversation, the model will return a chat completion response. * diff --git a/src/Contracts/Resources/RealtimeContract.php b/src/Contracts/Resources/RealtimeContract.php new file mode 100644 index 00000000..cb2b216e --- /dev/null +++ b/src/Contracts/Resources/RealtimeContract.php @@ -0,0 +1,29 @@ + $parameters + */ + public function token(array $parameters = []): SessionResponse; + + /** + * Create an ephemeral API token for real time transcription sessions. + * + * @see https://platform.openai.com/docs/api-reference/realtime-sessions/create-transcription + * + * @param array $parameters + */ + public function transcribeToken(array $parameters = []): TranscriptionSessionResponse; +} diff --git a/src/Resources/Realtime.php b/src/Resources/Realtime.php new file mode 100644 index 00000000..5d536fcb --- /dev/null +++ b/src/Resources/Realtime.php @@ -0,0 +1,54 @@ + $parameters + */ + public function token(array $parameters = []): SessionResponse + { + $payload = Payload::create('realtime/sessions', $parameters); + + /** @var Response $response */ + $response = $this->transporter->requestObject($payload); + + return SessionResponse::from($response->data()); + } + + /** + * Create an ephemeral API token for real time transcription sessions. + * + * @see https://platform.openai.com/docs/api-reference/realtime-sessions/create-transcription + * + * @param array $parameters + */ + public function transcribeToken(array $parameters = []): TranscriptionSessionResponse + { + $payload = Payload::create('realtime/transcription_sessions', $parameters); + + /** @var Response $response */ + $response = $this->transporter->requestObject($payload); + + return TranscriptionSessionResponse::from($response->data()); + } +} diff --git a/src/Responses/Realtime/Session/ClientSecret.php b/src/Responses/Realtime/Session/ClientSecret.php new file mode 100644 index 00000000..1cb90100 --- /dev/null +++ b/src/Responses/Realtime/Session/ClientSecret.php @@ -0,0 +1,51 @@ + + */ +final class ClientSecret implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly int $expiresAt, + public readonly string $value, + ) {} + + /** + * @param ClientSecretType $attributes + */ + public static function from(array $attributes): self + { + return new self( + expiresAt: $attributes['expires_at'], + value: $attributes['value'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'expires_at' => $this->expiresAt, + 'value' => $this->value, + ]; + } +} diff --git a/src/Responses/Realtime/Session/InputAudioTranscription.php b/src/Responses/Realtime/Session/InputAudioTranscription.php new file mode 100644 index 00000000..92277750 --- /dev/null +++ b/src/Responses/Realtime/Session/InputAudioTranscription.php @@ -0,0 +1,51 @@ + + */ +final class InputAudioTranscription implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param "whisper-1" $model + */ + private function __construct( + public readonly string $model + ) {} + + /** + * @param InputAudioTranscriptionType $attributes + */ + public static function from(array $attributes): self + { + return new self( + model: $attributes['model'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'model' => $this->model, + ]; + } +} diff --git a/src/Responses/Realtime/Session/TurnDetection.php b/src/Responses/Realtime/Session/TurnDetection.php new file mode 100644 index 00000000..7ef6eee4 --- /dev/null +++ b/src/Responses/Realtime/Session/TurnDetection.php @@ -0,0 +1,60 @@ + + */ +final class TurnDetection implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'server_vad' $type + */ + private function __construct( + public readonly int $prefixPaddingMs, + public readonly int $silenceDurationMs, + public readonly float $threshold, + public readonly string $type, + ) {} + + /** + * @param TurnDetectionType $attributes + */ + public static function from(array $attributes): self + { + return new self( + prefixPaddingMs: $attributes['prefix_padding_ms'], + silenceDurationMs: $attributes['silence_duration_ms'], + threshold: $attributes['threshold'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'prefix_padding_ms' => $this->prefixPaddingMs, + 'silence_duration_ms' => $this->silenceDurationMs, + 'threshold' => $this->threshold, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Realtime/SessionResponse.php b/src/Responses/Realtime/SessionResponse.php new file mode 100644 index 00000000..0e4efc32 --- /dev/null +++ b/src/Responses/Realtime/SessionResponse.php @@ -0,0 +1,113 @@ +, output_audio_format: 'pcm16'|'g711_ulaw'|'g711_alaw', temperature: float, tool_choice: 'auto'|'none'|'required', tools: array, turn_detection: TurnDetectionType|null, voice: 'alloy'|'ash'|'ballad'|'coral'|'echo'|'sage'|'shimmer'|'verse'} + * + * @implements ResponseContract + */ +final class SessionResponse implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'pcm16'|'g711_ulaw'|'g711_alaw' $inputAudioFormat + * @param int|'inf' $maxResponseOutputTokens + * @param array $modalities + * @param 'pcm16'|'g711_ulaw'|'g711_alaw' $outputAudioFormat + * @param 'auto'|'none'|'required' $toolChoice + * @param array $tools + * @param 'alloy'|'ash'|'ballad'|'coral'|'echo'|'sage'|'shimmer'|'verse' $voice + */ + private function __construct( + public readonly ClientSecret $clientSecret, + public readonly string $inputAudioFormat, + public readonly ?InputAudioTranscription $inputAudioTranscription, + public readonly string $instructions, + public readonly int|string $maxResponseOutputTokens, + public readonly array $modalities, + public readonly string $outputAudioFormat, + public readonly float $temperature, + public readonly string $toolChoice, + public readonly array $tools, + public readonly ?TurnDetection $turnDetection, + public readonly string $voice, + ) {} + + /** + * @param SessionType $attributes + */ + public static function from(array $attributes): self + { + $tools = array_map( + fn (array $tool): FunctionTool => match ($tool['type']) { + 'function' => FunctionTool::from($tool), + }, + $attributes['tools'] + ); + + return new self( + clientSecret: ClientSecret::from($attributes['client_secret']), + inputAudioFormat: $attributes['input_audio_format'], + inputAudioTranscription: isset($attributes['input_audio_transcription']) + ? InputAudioTranscription::from($attributes['input_audio_transcription']) + : null, + instructions: $attributes['instructions'], + maxResponseOutputTokens: $attributes['max_response_output_tokens'], + modalities: $attributes['modalities'], + outputAudioFormat: $attributes['output_audio_format'], + temperature: $attributes['temperature'], + toolChoice: $attributes['tool_choice'], + tools: $tools, + turnDetection: isset($attributes['turn_detection']) + ? TurnDetection::from($attributes['turn_detection']) + : null, + voice: $attributes['voice'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'client_secret' => $this->clientSecret->toArray(), + 'input_audio_format' => $this->inputAudioFormat, + 'input_audio_transcription' => $this->inputAudioTranscription?->toArray(), + 'instructions' => $this->instructions, + 'max_response_output_tokens' => $this->maxResponseOutputTokens, + 'modalities' => $this->modalities, + 'output_audio_format' => $this->outputAudioFormat, + 'temperature' => $this->temperature, + 'tool_choice' => $this->toolChoice, + 'tools' => array_map( + static fn (FunctionTool $tool): array => $tool->toArray(), + $this->tools, + ), + 'turn_detection' => $this->turnDetection?->toArray(), + 'voice' => $this->voice, + ]; + } +} diff --git a/src/Responses/Realtime/Tools/FunctionTool.php b/src/Responses/Realtime/Tools/FunctionTool.php new file mode 100644 index 00000000..f0d807eb --- /dev/null +++ b/src/Responses/Realtime/Tools/FunctionTool.php @@ -0,0 +1,61 @@ +, type: 'function'} + * + * @implements ResponseContract + */ +final class FunctionTool implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $parameters + * @param 'function' $type + */ + private function __construct( + public readonly string $description, + public readonly string $name, + public readonly array $parameters, + public readonly string $type, + ) {} + + /** + * @param FunctionToolType $attributes + */ + public static function from(array $attributes): self + { + return new self( + description: $attributes['description'], + name: $attributes['name'], + parameters: $attributes['parameters'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'description' => $this->description, + 'name' => $this->name, + 'parameters' => $this->parameters, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Realtime/TranscriptionSession/InputAudioTranscription.php b/src/Responses/Realtime/TranscriptionSession/InputAudioTranscription.php new file mode 100644 index 00000000..71d9b87a --- /dev/null +++ b/src/Responses/Realtime/TranscriptionSession/InputAudioTranscription.php @@ -0,0 +1,57 @@ + + */ +final class InputAudioTranscription implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'gpt-4o-transcribe'|'gpt-4o-mini-transcribe'|"whisper-1" $model + */ + private function __construct( + public readonly string $language, + public readonly string $model, + public readonly string $prompt, + ) {} + + /** + * @param InputAudioTranscriptionType $attributes + */ + public static function from(array $attributes): self + { + return new self( + language: $attributes['language'], + model: $attributes['model'], + prompt: $attributes['prompt'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'language' => $this->language, + 'model' => $this->model, + 'prompt' => $this->prompt, + ]; + } +} diff --git a/src/Responses/Realtime/TranscriptionSessionResponse.php b/src/Responses/Realtime/TranscriptionSessionResponse.php new file mode 100644 index 00000000..4243bc12 --- /dev/null +++ b/src/Responses/Realtime/TranscriptionSessionResponse.php @@ -0,0 +1,77 @@ +|null, turn_detection: TurnDetectionType|null} + * + * @implements ResponseContract + */ +final class TranscriptionSessionResponse implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'pcm16'|'g711_ulaw'|'g711_alaw' $inputAudioFormat + * @param array|null $modalities + */ + private function __construct( + public readonly ClientSecret $clientSecret, + public readonly string $inputAudioFormat, + public readonly ?InputAudioTranscription $inputAudioTranscription, + public readonly ?array $modalities, + public readonly ?TurnDetection $turnDetection, + ) {} + + /** + * @param TranscriptionSessionType $attributes + */ + public static function from(array $attributes): self + { + return new self( + clientSecret: ClientSecret::from($attributes['client_secret']), + inputAudioFormat: $attributes['input_audio_format'], + inputAudioTranscription: isset($attributes['input_audio_transcription']) + ? InputAudioTranscription::from($attributes['input_audio_transcription']) + : null, + modalities: $attributes['modalities'] ?? null, + turnDetection: isset($attributes['turn_detection']) + ? TurnDetection::from($attributes['turn_detection']) + : null, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'client_secret' => $this->clientSecret->toArray(), + 'input_audio_format' => $this->inputAudioFormat, + 'input_audio_transcription' => $this->inputAudioTranscription?->toArray(), + 'modalities' => $this->modalities, + 'turn_detection' => $this->turnDetection?->toArray(), + ]; + } +} diff --git a/src/Testing/ClientFake.php b/src/Testing/ClientFake.php index c6e07481..4fcf826c 100644 --- a/src/Testing/ClientFake.php +++ b/src/Testing/ClientFake.php @@ -21,6 +21,7 @@ use OpenAI\Testing\Resources\ImagesTestResource; use OpenAI\Testing\Resources\ModelsTestResource; use OpenAI\Testing\Resources\ModerationsTestResource; +use OpenAI\Testing\Resources\RealtimeTestResource; use OpenAI\Testing\Resources\ResponsesTestResource; use OpenAI\Testing\Resources\ThreadsTestResource; use OpenAI\Testing\Resources\VectorStoresTestResource; @@ -138,6 +139,11 @@ public function responses(): ResponsesTestResource return new ResponsesTestResource($this); } + public function realtime(): RealtimeTestResource + { + return new RealtimeTestResource($this); + } + public function completions(): CompletionsTestResource { return new CompletionsTestResource($this); diff --git a/src/Testing/Resources/RealtimeTestResource.php b/src/Testing/Resources/RealtimeTestResource.php new file mode 100644 index 00000000..6addab4a --- /dev/null +++ b/src/Testing/Resources/RealtimeTestResource.php @@ -0,0 +1,29 @@ +record(__FUNCTION__, func_get_args()); + } + + public function transcribeToken(array $parameters = []): TranscriptionSessionResponse + { + return $this->record(__FUNCTION__, func_get_args()); + } +} diff --git a/src/Testing/Responses/Fixtures/Realtime/SessionResponseFixture.php b/src/Testing/Responses/Fixtures/Realtime/SessionResponseFixture.php new file mode 100644 index 00000000..3af1d3ac --- /dev/null +++ b/src/Testing/Responses/Fixtures/Realtime/SessionResponseFixture.php @@ -0,0 +1,32 @@ + [ + 'expires_at' => 1735680000, + 'value' => 'ek_secret_123', + ], + 'input_audio_format' => 'pcm16', + 'input_audio_transcription' => null, + 'instructions' => 'Your knowledge cutoff is 2023-10. You are a helpful assistant.', + 'max_response_output_tokens' => 'inf', + 'modalities' => [ + 'audio', + 'text', + ], + 'output_audio_format' => 'pcm16', + 'temperature' => 0.7, + 'tool_choice' => 'auto', + 'tools' => [], + 'turn_detection' => [ + 'prefix_padding_ms' => 100, + 'silence_duration_ms' => 500, + 'threshold' => 0.5, + 'type' => 'server_vad', + ], + 'voice' => 'alloy', + ]; +} diff --git a/src/Testing/Responses/Fixtures/Realtime/TranscriptionSessionResponseFixture.php b/src/Testing/Responses/Fixtures/Realtime/TranscriptionSessionResponseFixture.php new file mode 100644 index 00000000..caf4cd5b --- /dev/null +++ b/src/Testing/Responses/Fixtures/Realtime/TranscriptionSessionResponseFixture.php @@ -0,0 +1,22 @@ + [ + 'expires_at' => 1735680000, + 'value' => 'ek_secret_123', + ], + 'input_audio_format' => 'pcm16', + 'input_audio_transcription' => null, + 'modalities' => null, + 'turn_detection' => [ + 'prefix_padding_ms' => 300, + 'silence_duration_ms' => 200, + 'threshold' => 0.5, + 'type' => 'server_vad', + ], + ]; +} diff --git a/tests/Fixtures/Realtime.php b/tests/Fixtures/Realtime.php new file mode 100644 index 00000000..b027b71c --- /dev/null +++ b/tests/Fixtures/Realtime.php @@ -0,0 +1,55 @@ + + */ +function sessionResponseResource(): array +{ + return [ + 'client_secret' => [ + 'expires_at' => 1735680000, + 'value' => 'ek_secret_123', + ], + 'input_audio_format' => 'pcm16', + 'input_audio_transcription' => null, + 'instructions' => 'Your knowledge cutoff is 2023-10. You are a helpful assistant.', + 'max_response_output_tokens' => 'inf', + 'modalities' => [ + 'audio', + 'text', + ], + 'output_audio_format' => 'pcm16', + 'temperature' => 0.7, + 'tool_choice' => 'auto', + 'tools' => [], + 'turn_detection' => [ + 'prefix_padding_ms' => 100, + 'silence_duration_ms' => 500, + 'threshold' => 0.5, + 'type' => 'server_vad', + ], + 'voice' => 'alloy', + ]; +} + +/** + * @return array + */ +function transcriptionSessionResponseResource(): array +{ + return [ + 'client_secret' => [ + 'expires_at' => 1735680000, + 'value' => 'ek_secret_345', + ], + 'input_audio_format' => 'pcm16', + 'input_audio_transcription' => null, + 'modalities' => null, + 'turn_detection' => [ + 'prefix_padding_ms' => 300, + 'silence_duration_ms' => 200, + 'threshold' => 0.5, + 'type' => 'server_vad', + ], + ]; +} diff --git a/tests/Resources/Realtime.php b/tests/Resources/Realtime.php new file mode 100644 index 00000000..478a446b --- /dev/null +++ b/tests/Resources/Realtime.php @@ -0,0 +1,24 @@ +realtime()->token(); + + expect($result) + ->toBeInstanceOf(SessionResponse::class) + ->clientSecret->value->toBe('ek_secret_123'); +}); + +test('transcription token', function () { + $client = mockClient('POST', 'realtime/transcription_sessions', [], \OpenAI\ValueObjects\Transporter\Response::from(transcriptionSessionResponseResource(), metaHeaders())); + + $result = $client->realtime()->transcribeToken(); + + expect($result) + ->toBeInstanceOf(TranscriptionSessionResponse::class) + ->clientSecret->value->toBe('ek_secret_345'); +}); diff --git a/tests/Responses/Realtime/SessionResponse.php b/tests/Responses/Realtime/SessionResponse.php new file mode 100644 index 00000000..5b78d7b5 --- /dev/null +++ b/tests/Responses/Realtime/SessionResponse.php @@ -0,0 +1,24 @@ +toBeInstanceOf(SessionResponse::class) + ->clientSecret->toBeInstanceOf(ClientSecret::class) + ->inputAudioFormat->toBe('pcm16') + ->inputAudioTranscription->toBeNull() + ->instructions->toBe('Your knowledge cutoff is 2023-10. You are a helpful assistant.') + ->maxResponseOutputTokens->toBe('inf') + ->modalities->toBe(['audio', 'text']) + ->outputAudioFormat->toBe('pcm16') + ->temperature->toBe(0.7) + ->toolChoice->toBe('auto') + ->tools->toBeArray() + ->turnDetection->toBeInstanceOf(TurnDetection::class) + ->voice->toBe('alloy'); +}); diff --git a/tests/Responses/Realtime/TranscriptionSessionResponse.php b/tests/Responses/Realtime/TranscriptionSessionResponse.php new file mode 100644 index 00000000..7e57fc50 --- /dev/null +++ b/tests/Responses/Realtime/TranscriptionSessionResponse.php @@ -0,0 +1,17 @@ +toBeInstanceOf(TranscriptionSessionResponse::class) + ->clientSecret->toBeInstanceOf(ClientSecret::class) + ->inputAudioFormat->toBe('pcm16') + ->inputAudioTranscription->toBeNull() + ->modalities->toBeNull() + ->turnDetection->toBeInstanceOf(TurnDetection::class); +}); diff --git a/tests/Testing/Resources/RealtimeTestResource.php b/tests/Testing/Resources/RealtimeTestResource.php new file mode 100644 index 00000000..92319d8a --- /dev/null +++ b/tests/Testing/Resources/RealtimeTestResource.php @@ -0,0 +1,30 @@ +realtime()->token(); + + $fake->assertSent(Realtime::class, function ($method) { + return $method === 'token'; + }); +}); + +it('records a realtime token transcription request', function () { + $fake = new ClientFake([ + TranscriptionSessionResponse::fake(), + ]); + + $fake->realtime()->transcribeToken(); + + $fake->assertSent(Realtime::class, function ($method) { + return $method === 'transcribeToken'; + }); +}); From 04e75a29590301090f6edbe8e3dbfde6e99882df Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Mon, 9 Jun 2025 16:22:20 -0400 Subject: [PATCH 13/14] docs(OpenAI): use proper header notation for Responses API (#596) --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 609f9f53..44dddcd4 100644 --- a/README.md +++ b/README.md @@ -227,7 +227,7 @@ foreach ($stream as $response) { } ``` -### `retrieve` +#### `retrieve` Retrieves a model response with the given ID. @@ -254,7 +254,7 @@ $response->truncation; // 'disabled' $response->toArray(); // ['id' => 'resp_67ccd2bed1ec8190b14f964abc054267', ...] ``` -### `cancel` +#### `cancel` Cancel a model response (background request) with the given ID. @@ -267,7 +267,7 @@ $response->status; // 'canceled' $response->toArray(); // ['id' => 'resp_67ccd2bed1ec8190b14f964abc054267', 'status' => 'canceled', ...] ``` -### `delete` +#### `delete` Deletes a model response with the given ID. @@ -281,7 +281,7 @@ $response->deleted; // true $response->toArray(); // ['id' => 'resp_67ccd2bed1ec8190b14f964abc054267', 'deleted' => true, ...] ``` -### `list` +#### `list` Lists input items for a response with the given ID. All events and their payloads can be found in [OpenAI docs](https://platform.openai.com/docs/api-reference/responses/list). From ffaafce2379d1694accf72d305e6220f1bd9c5dd Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Mon, 9 Jun 2025 17:25:51 -0400 Subject: [PATCH 14/14] docs(OpenAI): add realtime key docs (#597) --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 44dddcd4..7c6adc8a 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ If you or your business relies on this package, it's important to support the de - [Vector Stores Files Resource](#vector-store-files-resource) - [Vector Stores File Batches Resource](#vector-store-file-batches-resource) - [Batches Resource](#batches-resource) + - [Realtime Ephemeral Keys](#realtime-ephemeral-keys) - [FineTunes Resource (deprecated)](#finetunes-resource-deprecated) - [Edits Resource (deprecated)](#edits-resource-deprecated) - [Meta Information](#meta-information) @@ -2274,6 +2275,30 @@ foreach ($response->data as $result) { $response->toArray(); // ['object' => 'list', ...]] ``` +### Realtime Ephemeral Keys + +#### `token` + +Create an ephemeral API token for real time sessions. + +```php +$response = $client->realtime()->token(); + +$response->clientSecret->value // 'ek-1234567890abcdefg' +$response->clientSecret->expiresAt // 1717703267 +``` + +#### `transcribeToken` + +Create an ephemeral API token for real time transcription sessions. + +```php +$response = $client->realtime()->transcribeToken(); + +$response->clientSecret->value // 'et-1234567890abcdefg' +$response->clientSecret->expiresAt // 1717703267 +``` + ### `Edits` Resource (deprecated) > [!WARNING]