From 7493f3fb09981e1db2579db7a8127cf3e03d1ad3 Mon Sep 17 00:00:00 2001 From: Louis Bels Date: Wed, 27 Aug 2025 21:57:30 +0200 Subject: [PATCH 01/11] fix(Scaleway): Handle optional attributes in CreateResponse (#662) * Handle optional attributes in CreateResponse * Add tests for `store` and `text` field handling in `CreateResponse` * Make `store` and `text` fields optional in `CreateResponseType` definition --- src/Responses/Responses/CreateResponse.php | 12 +++-- tests/Responses/Responses/CreateResponse.php | 50 ++++++++++++++++++++ 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/Responses/Responses/CreateResponse.php b/src/Responses/Responses/CreateResponse.php index b1fbafd6..86952e16 100644 --- a/src/Responses/Responses/CreateResponse.php +++ b/src/Responses/Responses/CreateResponse.php @@ -64,7 +64,7 @@ * @phpstan-type ToolChoiceType 'none'|'auto'|'required'|FunctionToolChoiceType|HostedToolChoiceType * @phpstan-type ToolsType array * @phpstan-type OutputType array - * @phpstan-type CreateResponseType array{id: string, background?: bool|null, object: 'response', created_at: int, status: 'completed'|'failed'|'in_progress'|'incomplete', error: ErrorType|null, incomplete_details: IncompleteDetailsType|null, instructions: InstructionsType, max_output_tokens: int|null, max_tool_calls?: int|null, model: string, output: OutputType, output_text: string|null, parallel_tool_calls: bool, previous_response_id: string|null, prompt: ReferencePromptObjectType|null, prompt_cache_key?: string|null, reasoning: ReasoningType|null, safety_identifier?: string|null, service_tier?: string|null, store: bool, temperature: float|null, text: ResponseFormatType, tool_choice: ToolChoiceType, tools: ToolsType, top_logprobs?: int|null, top_p: float|null, truncation: 'auto'|'disabled'|null, usage: UsageType|null, user: string|null, verbosity: string|null, metadata: array|null} + * @phpstan-type CreateResponseType array{id: string, background?: bool|null, object: 'response', created_at: int, status: 'completed'|'failed'|'in_progress'|'incomplete', error: ErrorType|null, incomplete_details: IncompleteDetailsType|null, instructions: InstructionsType, max_output_tokens: int|null, max_tool_calls?: int|null, model: string, output: OutputType, output_text: string|null, parallel_tool_calls: bool, previous_response_id: string|null, prompt: ReferencePromptObjectType|null, prompt_cache_key?: string|null, reasoning: ReasoningType|null, safety_identifier?: string|null, service_tier?: string|null, store?: bool|null, temperature: float|null, text?: ResponseFormatType|null, tool_choice: ToolChoiceType, tools: ToolsType, top_logprobs?: int|null, top_p: float|null, truncation: 'auto'|'disabled'|null, usage: UsageType|null, user: string|null, verbosity: string|null, metadata: array|null} * * @implements ResponseContract */ @@ -110,7 +110,7 @@ private function __construct( public readonly ?CreateResponseReasoning $reasoning, public readonly bool $store, public readonly ?float $temperature, - public readonly CreateResponseFormat $text, + public readonly ?CreateResponseFormat $text, public readonly string|FunctionToolChoice|HostedToolChoice $toolChoice, public readonly array $tools, public readonly ?int $topLogProbs, @@ -206,9 +206,11 @@ public static function from(array $attributes, MetaInformation $meta): self reasoning: isset($attributes['reasoning']) ? CreateResponseReasoning::from($attributes['reasoning']) : null, - store: $attributes['store'], + store: $attributes['store'] ?? true, temperature: $attributes['temperature'], - text: CreateResponseFormat::from($attributes['text']), + text: isset($attributes['text']) + ? CreateResponseFormat::from($attributes['text']) + : null, toolChoice: $toolChoice, tools: $tools, topLogProbs: $attributes['top_logprobs'] ?? null, @@ -257,7 +259,7 @@ public function toArray(): array 'reasoning' => $this->reasoning?->toArray(), 'store' => $this->store, 'temperature' => $this->temperature, - 'text' => $this->text->toArray(), + 'text' => $this->text?->toArray(), 'tool_choice' => is_string($this->toolChoice) ? $this->toolChoice : $this->toolChoice->toArray(), diff --git a/tests/Responses/Responses/CreateResponse.php b/tests/Responses/Responses/CreateResponse.php index 370da4e4..37300b00 100644 --- a/tests/Responses/Responses/CreateResponse.php +++ b/tests/Responses/Responses/CreateResponse.php @@ -133,3 +133,53 @@ ->type->toBe('output_text') ->text->toBe('This is a basic message.'); }); + +test('from with missing store field defaults to true', function () { + $response = CreateResponse::fake(); + + expect($response) + ->toBeInstanceOf(CreateResponse::class) + ->store->toBeTrue(); +}); + +test('from with null store field defaults to true', function () { + $response = CreateResponse::fake(['store' => null]); + + expect($response) + ->toBeInstanceOf(CreateResponse::class) + ->store->toBeTrue(); +}); + +test('from with false store field', function () { + $response = CreateResponse::fake(['store' => false]); + + expect($response) + ->toBeInstanceOf(CreateResponse::class) + ->store->toBeFalse(); +}); + +test('from with missing text field', function () { + $response = CreateResponse::fake(['text' => null]); + + expect($response) + ->toBeInstanceOf(CreateResponse::class) + ->text->toBeNull(); +}); + +test('from with null text field', function () { + $response = CreateResponse::fake(['text' => null]); + + expect($response) + ->toBeInstanceOf(CreateResponse::class) + ->text->toBeNull(); +}); + +test('to array with null text field', function () { + $response = CreateResponse::fake(['text' => null]); + + $array = $response->toArray(); + + expect($array) + ->toBeArray() + ->text->toBeNull(); +}); From b30b049efa1e0acaa3d95e52c9cede0d5a4719e2 Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Thu, 28 Aug 2025 00:00:46 +0400 Subject: [PATCH 02/11] fix(OpenAI): Add `sequence_number` support to `OutputTextDelta`. (#664) --- src/Responses/Responses/Streaming/OutputTextDelta.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Responses/Responses/Streaming/OutputTextDelta.php b/src/Responses/Responses/Streaming/OutputTextDelta.php index 486eccbe..6d5123b4 100644 --- a/src/Responses/Responses/Streaming/OutputTextDelta.php +++ b/src/Responses/Responses/Streaming/OutputTextDelta.php @@ -12,7 +12,7 @@ use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @phpstan-type OutputTextType array{content_index: int, delta: string, item_id: string, output_index: int} + * @phpstan-type OutputTextType array{content_index: int, delta: string, item_id: string, output_index: int, sequence_number: int} * * @implements ResponseContract */ @@ -31,6 +31,7 @@ private function __construct( public readonly string $delta, public readonly string $itemId, public readonly int $outputIndex, + public readonly int $sequenceNumber, private readonly MetaInformation $meta, ) {} @@ -44,6 +45,7 @@ public static function from(array $attributes, MetaInformation $meta): self delta: $attributes['delta'], itemId: $attributes['item_id'], outputIndex: $attributes['output_index'], + sequenceNumber: $attributes['sequence_number'], meta: $meta, ); } @@ -58,6 +60,7 @@ public function toArray(): array 'delta' => $this->delta, 'item_id' => $this->itemId, 'output_index' => $this->outputIndex, + 'sequence_number' => $this->sequenceNumber, ]; } } From 8677a94d64602465d773be4b26a2b19e8a9be8cc Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Wed, 27 Aug 2025 16:34:18 -0400 Subject: [PATCH 03/11] fix(OpenAI): support mcp error objects in response api (#661) * fix(OpenAI): support mcp error objects in response api * fix(OpenAI): rework McpGenericException into standalone * fix(OpenAI): remove unneeded import --- .../Responses/McpGenericResponseError.php | 54 +++++++++++++++++++ .../Responses/Output/OutputMcpCall.php | 15 ++++-- tests/Fixtures/Responses.php | 27 ++++++++++ .../Responses/Output/OutputMcpCall.php | 19 +++++++ 4 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 src/Responses/Responses/McpGenericResponseError.php diff --git a/src/Responses/Responses/McpGenericResponseError.php b/src/Responses/Responses/McpGenericResponseError.php new file mode 100644 index 00000000..49ec4986 --- /dev/null +++ b/src/Responses/Responses/McpGenericResponseError.php @@ -0,0 +1,54 @@ +|null} + * + * @implements ResponseContract + */ +final class McpGenericResponseError implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array|null $content + */ + protected function __construct( + public readonly string $type, + public readonly ?array $content = null, + ) {} + + /** + * @param McpErrorType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: (string) $attributes['type'], + content: $attributes['content'] ?? null, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'content' => $this->content, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputMcpCall.php b/src/Responses/Responses/Output/OutputMcpCall.php index d5375e50..9fbf250e 100644 --- a/src/Responses/Responses/Output/OutputMcpCall.php +++ b/src/Responses/Responses/Output/OutputMcpCall.php @@ -7,12 +7,14 @@ use OpenAI\Contracts\ResponseContract; use OpenAI\Responses\Concerns\ArrayAccessible; use OpenAI\Responses\Responses\GenericResponseError; +use OpenAI\Responses\Responses\McpGenericResponseError; use OpenAI\Testing\Responses\Concerns\Fakeable; /** * @phpstan-import-type ErrorType from GenericResponseError + * @phpstan-import-type McpErrorType from McpGenericResponseError * - * @phpstan-type OutputMcpCallType array{id: string, server_label: string, type: 'mcp_call', approval_request_id: ?string, arguments: string, error: string|ErrorType|null, name: string, output: ?string} + * @phpstan-type OutputMcpCallType array{id: string, server_label: string, type: 'mcp_call', approval_request_id: ?string, arguments: string, error: string|McpErrorType|ErrorType|null, name: string, output: ?string} * * @implements ResponseContract */ @@ -35,7 +37,7 @@ private function __construct( public readonly string $arguments, public readonly string $name, public readonly ?string $approvalRequestId = null, - public readonly ?GenericResponseError $error = null, + public readonly McpGenericResponseError|GenericResponseError|null $error = null, public readonly ?string $output = null, ) {} @@ -45,10 +47,13 @@ private function __construct( public static function from(array $attributes): self { // OpenAI has odd structure (presumably a bug) where the errorType can sometimes be a full-fledged HTTP error object. - // As MCP calls are valid HTTP requests - we need to handle strings & objects here. + // They can also be a full-fledged MCP error object. + // They can also just be a string message. So we need to handle all three cases. $errorType = null; if (isset($attributes['error'])) { - if (is_array($attributes['error'])) { + if (is_array($attributes['error']) && isset($attributes['error']['content'])) { + $errorType = McpGenericResponseError::from($attributes['error']); + } elseif (is_array($attributes['error']) && isset($attributes['error']['message'])) { $errorType = GenericResponseError::from($attributes['error']); } elseif (is_string($attributes['error'])) { $errorType = GenericResponseError::from([ @@ -82,7 +87,7 @@ public function toArray(): array 'arguments' => $this->arguments, 'name' => $this->name, 'approval_request_id' => $this->approvalRequestId, - 'error' => $this->error instanceof GenericResponseError + 'error' => $this->error instanceof GenericResponseError || $this->error instanceof McpGenericResponseError ? $this->error->toArray() : $this->error, 'output' => $this->output, diff --git a/tests/Fixtures/Responses.php b/tests/Fixtures/Responses.php index 8ecb544e..7f0408cf 100644 --- a/tests/Fixtures/Responses.php +++ b/tests/Fixtures/Responses.php @@ -364,6 +364,33 @@ function outputMcpListTools(): array ]; } +/** + * @return array + */ +function outputMcpErrorCallToolExecution(): array +{ + return [ + 'id' => 'mcp_68ae0539ede081a096e9cc4526aadc8200b5e200d643ebad', + 'type' => 'mcp_call', + 'approval_request_id' => null, + 'arguments' => '{"value":"test"}', + 'error' => [ + 'type' => 'mcp_tool_execution_error', + 'content' => [ + [ + 'type' => 'text', + 'text' => '[POST] "undefined": Invalid URL: undefined', + 'annotations' => null, + 'meta' => null, + ], + ], + ], + 'name' => 'deploy-html', + 'output' => null, + 'server_label' => 'deploy-html', + ]; +} + /** * @return array */ diff --git a/tests/Responses/Responses/Output/OutputMcpCall.php b/tests/Responses/Responses/Output/OutputMcpCall.php index 6d4d8cc9..50edeeed 100644 --- a/tests/Responses/Responses/Output/OutputMcpCall.php +++ b/tests/Responses/Responses/Output/OutputMcpCall.php @@ -1,6 +1,7 @@ toBeInstanceOf(OutputMcpCall::class) + ->id->toBe('mcp_68ae0539ede081a096e9cc4526aadc8200b5e200d643ebad') + ->type->toBe('mcp_call') + ->approvalRequestId->toBeNull() + ->arguments->toBe('{"value":"test"}') + ->name->toBe('deploy-html') + ->output->toBeNull() + ->serverLabel->toBe('deploy-html') + ->error->toBeInstanceOf(McpGenericResponseError::class) + ->and($response->error) + ->type->toBe('mcp_tool_execution_error') + ->content->toBeArray(); +}); + test('as array accessible', function () { $response = OutputMcpCall::from(outputMcpCall()); From a38f3340b5f9eb8e56ef7ecf1437da66bfc5e56a Mon Sep 17 00:00:00 2001 From: ymktmk <73768462+ymktmk@users.noreply.github.com> Date: Fri, 29 Aug 2025 21:11:24 +0900 Subject: [PATCH 04/11] fix(OpenA): add 'web_search' as valid tool choice for Response API(#665) --- src/Responses/Responses/CreateResponse.php | 4 ++-- src/Responses/Responses/RetrieveResponse.php | 4 ++-- src/Responses/Responses/Tool/WebSearchTool.php | 4 ++-- src/Responses/Responses/ToolChoice/HostedToolChoice.php | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Responses/Responses/CreateResponse.php b/src/Responses/Responses/CreateResponse.php index 86952e16..94eef4ac 100644 --- a/src/Responses/Responses/CreateResponse.php +++ b/src/Responses/Responses/CreateResponse.php @@ -147,7 +147,7 @@ public static function from(array $attributes, MetaInformation $meta): self $toolChoice = is_array($attributes['tool_choice']) ? match ($attributes['tool_choice']['type']) { - 'file_search', 'web_search_preview', 'computer_use_preview' => HostedToolChoice::from($attributes['tool_choice']), + 'file_search', 'web_search', 'web_search_preview', 'computer_use_preview' => HostedToolChoice::from($attributes['tool_choice']), 'function' => FunctionToolChoice::from($attributes['tool_choice']), } : $attributes['tool_choice']; @@ -155,7 +155,7 @@ public static function from(array $attributes, MetaInformation $meta): self $tools = array_map( fn (array $tool): ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool => match ($tool['type']) { 'file_search' => FileSearchTool::from($tool), - 'web_search_preview', 'web_search_preview_2025_03_11' => WebSearchTool::from($tool), + 'web_search', '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), diff --git a/src/Responses/Responses/RetrieveResponse.php b/src/Responses/Responses/RetrieveResponse.php index 0606f960..bdc117dd 100644 --- a/src/Responses/Responses/RetrieveResponse.php +++ b/src/Responses/Responses/RetrieveResponse.php @@ -148,7 +148,7 @@ public static function from(array $attributes, MetaInformation $meta): self $toolChoice = is_array($attributes['tool_choice']) ? match ($attributes['tool_choice']['type']) { - 'file_search', 'web_search_preview', 'computer_use_preview' => HostedToolChoice::from($attributes['tool_choice']), + 'file_search', 'web_search', 'web_search_preview', 'computer_use_preview' => HostedToolChoice::from($attributes['tool_choice']), 'function' => FunctionToolChoice::from($attributes['tool_choice']), } : $attributes['tool_choice']; @@ -156,7 +156,7 @@ public static function from(array $attributes, MetaInformation $meta): self $tools = array_map( fn (array $tool): ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool => match ($tool['type']) { 'file_search' => FileSearchTool::from($tool), - 'web_search_preview', 'web_search_preview_2025_03_11' => WebSearchTool::from($tool), + 'web_search', '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), diff --git a/src/Responses/Responses/Tool/WebSearchTool.php b/src/Responses/Responses/Tool/WebSearchTool.php index c93cae06..41f0d2c1 100644 --- a/src/Responses/Responses/Tool/WebSearchTool.php +++ b/src/Responses/Responses/Tool/WebSearchTool.php @@ -11,7 +11,7 @@ /** * @phpstan-import-type UserLocationType from WebSearchUserLocation * - * @phpstan-type WebSearchToolType array{type: 'web_search_preview'|'web_search_preview_2025_03_11', search_context_size: 'low'|'medium'|'high', user_location: ?UserLocationType} + * @phpstan-type WebSearchToolType array{type: 'web_search'|'web_search_preview'|'web_search_preview_2025_03_11', search_context_size: 'low'|'medium'|'high', user_location: ?UserLocationType} * * @implements ResponseContract */ @@ -25,7 +25,7 @@ final class WebSearchTool implements ResponseContract use Fakeable; /** - * @param 'web_search_preview'|'web_search_preview_2025_03_11' $type + * @param 'web_search'|'web_search_preview'|'web_search_preview_2025_03_11' $type * @param 'low'|'medium'|'high' $searchContextSize */ private function __construct( diff --git a/src/Responses/Responses/ToolChoice/HostedToolChoice.php b/src/Responses/Responses/ToolChoice/HostedToolChoice.php index 3b012c13..56e2895e 100644 --- a/src/Responses/Responses/ToolChoice/HostedToolChoice.php +++ b/src/Responses/Responses/ToolChoice/HostedToolChoice.php @@ -9,7 +9,7 @@ use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @phpstan-type HostedToolChoiceType array{type: 'file_search'|'web_search_preview'|'computer_use_preview'} + * @phpstan-type HostedToolChoiceType array{type: 'file_search'|'web_search'|'web_search_preview'|'computer_use_preview'} * * @implements ResponseContract */ @@ -23,7 +23,7 @@ final class HostedToolChoice implements ResponseContract use Fakeable; /** - * @param 'file_search'|'web_search_preview'|'computer_use_preview' $type + * @param 'file_search'|'web_search'|'web_search_preview'|'computer_use_preview' $type */ private function __construct( public readonly string $type, From 8f8414adb3bc92a694d7f72e542788fc8da9ab4f Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Sat, 30 Aug 2025 07:09:26 -0400 Subject: [PATCH 05/11] fix(OpenAI): Add missing properties to ListInputItems call (#668) * fix(OpenAI): add missing properties to ListItems for Response API * test(OpenAI): augment tests for ListInputItems --- src/Responses/Responses/ListInputItems.php | 26 +++++++++++++++++--- tests/Fixtures/Responses.php | 9 +++++++ tests/Responses/Responses/ListInputItems.php | 1 + 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/Responses/Responses/ListInputItems.php b/src/Responses/Responses/ListInputItems.php index caff4ecb..018c2a98 100644 --- a/src/Responses/Responses/ListInputItems.php +++ b/src/Responses/Responses/ListInputItems.php @@ -12,10 +12,16 @@ use OpenAI\Responses\Responses\Input\ComputerToolCallOutput; use OpenAI\Responses\Responses\Input\FunctionToolCallOutput; use OpenAI\Responses\Responses\Input\InputMessage; +use OpenAI\Responses\Responses\Output\OutputCodeInterpreterToolCall; use OpenAI\Responses\Responses\Output\OutputComputerToolCall; use OpenAI\Responses\Responses\Output\OutputFileSearchToolCall; use OpenAI\Responses\Responses\Output\OutputFunctionToolCall; +use OpenAI\Responses\Responses\Output\OutputImageGenerationToolCall; +use OpenAI\Responses\Responses\Output\OutputMcpApprovalRequest; +use OpenAI\Responses\Responses\Output\OutputMcpCall; +use OpenAI\Responses\Responses\Output\OutputMcpListTools; use OpenAI\Responses\Responses\Output\OutputMessage; +use OpenAI\Responses\Responses\Output\OutputReasoning; use OpenAI\Responses\Responses\Output\OutputWebSearchToolCall; use OpenAI\Testing\Responses\Concerns\Fakeable; @@ -28,8 +34,14 @@ * @phpstan-import-type OutputWebSearchToolCallType from OutputWebSearchToolCall * @phpstan-import-type OutputFunctionToolCallType from OutputFunctionToolCall * @phpstan-import-type FunctionToolCallOutputType from FunctionToolCallOutput + * @phpstan-import-type OutputReasoningType from OutputReasoning + * @phpstan-import-type OutputMcpListToolsType from OutputMcpListTools + * @phpstan-import-type OutputMcpApprovalRequestType from OutputMcpApprovalRequest + * @phpstan-import-type OutputMcpCallType from OutputMcpCall + * @phpstan-import-type OutputImageGenerationToolCallType from OutputImageGenerationToolCall + * @phpstan-import-type OutputCodeInterpreterToolCallType from OutputCodeInterpreterToolCall * - * @phpstan-type ListInputItemsType array{data: array, first_id: string, has_more: bool, last_id: string, object: 'list'} + * @phpstan-type ListInputItemsType array{data: array, first_id: string, has_more: bool, last_id: string, object: 'list'} * * @implements ResponseContract */ @@ -42,7 +54,7 @@ final class ListInputItems implements ResponseContract, ResponseHasMetaInformati use HasMetaInformation; /** - * @param array $data + * @param array $data * @param 'list' $object */ private function __construct( @@ -60,7 +72,7 @@ private function __construct( 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']) { + fn (array $item): InputMessage|OutputMessage|OutputFileSearchToolCall|OutputComputerToolCall|ComputerToolCallOutput|OutputWebSearchToolCall|OutputFunctionToolCall|FunctionToolCallOutput|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall => match ($item['type']) { 'message' => $item['role'] === 'assistant' ? OutputMessage::from($item) : InputMessage::from($item), 'file_search_call' => OutputFileSearchToolCall::from($item), 'function_call' => OutputFunctionToolCall::from($item), @@ -68,6 +80,12 @@ public static function from(array $attributes, MetaInformation $meta): self 'web_search_call' => OutputWebSearchToolCall::from($item), 'computer_call' => OutputComputerToolCall::from($item), 'computer_call_output' => ComputerToolCallOutput::from($item), + 'reasoning' => OutputReasoning::from($item), + 'mcp_list_tools' => OutputMcpListTools::from($item), + 'mcp_approval_request' => OutputMcpApprovalRequest::from($item), + 'mcp_call' => OutputMcpCall::from($item), + 'image_generation_call' => OutputImageGenerationToolCall::from($item), + 'code_interpreter_call' => OutputCodeInterpreterToolCall::from($item), }, $attributes['data'], ); @@ -90,7 +108,7 @@ public function toArray(): array return [ 'object' => $this->object, 'data' => array_map( - fn (InputMessage|OutputMessage|OutputFileSearchToolCall|OutputComputerToolCall|ComputerToolCallOutput|OutputWebSearchToolCall|OutputFunctionToolCall|FunctionToolCallOutput $item): array => $item->toArray(), + fn (InputMessage|OutputMessage|OutputFileSearchToolCall|OutputFunctionToolCall|FunctionToolCallOutput|OutputWebSearchToolCall|OutputComputerToolCall|ComputerToolCallOutput|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall $item): array => $item->toArray(), $this->data, ), 'first_id' => $this->firstId, diff --git a/tests/Fixtures/Responses.php b/tests/Fixtures/Responses.php index 7f0408cf..c14e7462 100644 --- a/tests/Fixtures/Responses.php +++ b/tests/Fixtures/Responses.php @@ -217,6 +217,15 @@ function listInputItemsResource(): array 'object' => 'list', 'data' => [ inputMessage(), + outputBasicMessage(), + outputAnnotationMessage(), + outputMessageOnlyRefusal(), + outputAnnotationMessage(), + outputWebSearchToolCall(), + outputFileSearchToolCall(), + outputComputerToolCall(), + outputReasoning(), + outputCodeInterpreterToolCall(), ], 'first_id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', 'last_id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', diff --git a/tests/Responses/Responses/ListInputItems.php b/tests/Responses/Responses/ListInputItems.php index 95daaf47..bb182e39 100644 --- a/tests/Responses/Responses/ListInputItems.php +++ b/tests/Responses/Responses/ListInputItems.php @@ -10,6 +10,7 @@ ->toBeInstanceOf(ListInputItems::class) ->object->toBe('list') ->data->toBeArray() + ->data->toHaveCount(10) ->firstId->toBe('msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c') ->lastId->toBe('msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c') ->hasMore->toBeFalse() From aab80a997c7d75b246ef83c5fe8f2bc2c419e3c9 Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Sat, 30 Aug 2025 12:37:01 -0400 Subject: [PATCH 06/11] fix(OpenAI): handle null require_approval in Response API (#669) * fix(OpenAI): handle null require_approval in Response API * test(OpenAI): augment tests for null require_approval * chore: add types * chore: add pint --- .../Responses/Tool/RemoteMcpTool.php | 9 +++++--- tests/Fixtures/Responses.php | 22 +++++++++++++++++++ .../Responses/Tool/RemoteMcpTool.php | 8 +++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/Responses/Responses/Tool/RemoteMcpTool.php b/src/Responses/Responses/Tool/RemoteMcpTool.php index 9c49005c..00d2a6fb 100644 --- a/src/Responses/Responses/Tool/RemoteMcpTool.php +++ b/src/Responses/Responses/Tool/RemoteMcpTool.php @@ -49,9 +49,12 @@ public static function from(array $attributes): self { $requireApproval = $attributes['require_approval'] ?? null; if (is_array($requireApproval)) { - $requireApproval = array_map(function (array $approvalAttributes): McpToolNamesFilter { - return McpToolNamesFilter::from($approvalAttributes); - }, $requireApproval); + $requireApproval = array_map( + function (array $approvalAttributes): McpToolNamesFilter { + return McpToolNamesFilter::from($approvalAttributes); + }, + array_filter($requireApproval, fn (?array $item) => $item !== null) + ); } $allowedTools = $attributes['allowed_tools'] ?? null; diff --git a/tests/Fixtures/Responses.php b/tests/Fixtures/Responses.php index c14e7462..e50e0cbf 100644 --- a/tests/Fixtures/Responses.php +++ b/tests/Fixtures/Responses.php @@ -626,6 +626,28 @@ function toolRemoteMcp(): array ]; } +/** + * @return array + */ +function toolRemoveMcpRequireApproval(): array +{ + return [ + 'type' => 'mcp', + 'server_label' => 'My test MCP server', + 'server_url' => 'https://server.example.com/mcp', + 'require_approval' => [ + 'never' => [ + 'read_only' => null, + 'tool_names' => ['ask_question', 'read_wiki_structure'], + ], + 'always' => null, + ], + 'allowed_tools' => null, + 'headers' => null, + 'server_description' => null, + ]; +} + /** * @return array */ diff --git a/tests/Responses/Responses/Tool/RemoteMcpTool.php b/tests/Responses/Responses/Tool/RemoteMcpTool.php index 517cdf0c..dfda7ab7 100644 --- a/tests/Responses/Responses/Tool/RemoteMcpTool.php +++ b/tests/Responses/Responses/Tool/RemoteMcpTool.php @@ -45,6 +45,14 @@ ->requireApproval->toBeArray(); }); +test('from object as specific approved tools', function () { + $response = RemoteMcpTool::from(toolRemoveMcpRequireApproval()); + + expect($response) + ->toBeInstanceOf(RemoteMcpTool::class) + ->requireApproval->toBeArray(); +}); + test('from object as allowed_tools', function () { $payload = toolRemoteMcp(); $payload['allowed_tools'] = [ From 78320dfe3c4b8294d26b1540a10c6ff46060d147 Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Mon, 1 Sep 2025 06:22:36 -0400 Subject: [PATCH 07/11] fix(OpenAI): support nested file search properties (#670) * fix(OpenAI): support nested file search properties * test(OpenAI): augment file search for nested properties --- .../Tool/FileSearchCompoundFilter.php | 13 +++++-- tests/Fixtures/Responses.php | 39 +++++++++++++++++++ .../Responses/Tool/FileSearchTool.php | 17 ++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/Responses/Responses/Tool/FileSearchCompoundFilter.php b/src/Responses/Responses/Tool/FileSearchCompoundFilter.php index 5eacabec..f4a0fdca 100644 --- a/src/Responses/Responses/Tool/FileSearchCompoundFilter.php +++ b/src/Responses/Responses/Tool/FileSearchCompoundFilter.php @@ -11,7 +11,8 @@ /** * @phpstan-import-type ComparisonFilterType from FileSearchComparisonFilter * - * @phpstan-type CompoundFilterType array{filters: array, type: 'and'|'or'} + * @phpstan-type CompoundFilterNodeType array{filters: array, type: 'and'|'or'} + * @phpstan-type CompoundFilterType array{filters: array, type: 'and'|'or'} * * @implements ResponseContract */ @@ -25,7 +26,7 @@ final class FileSearchCompoundFilter implements ResponseContract use Fakeable; /** - * @param array $filters + * @param array $filters * @param 'and'|'or' $type */ private function __construct( @@ -39,7 +40,10 @@ private function __construct( public static function from(array $attributes): self { $filters = array_map( - static fn (array $filter): FileSearchComparisonFilter => FileSearchComparisonFilter::from($filter), + static fn (array $filter): FileSearchComparisonFilter|FileSearchCompoundFilter => match ($filter['type']) { + 'eq', 'ne', 'gt', 'gte', 'lt', 'lte' => FileSearchComparisonFilter::from($filter), + 'and', 'or' => FileSearchCompoundFilter::from($filter), + }, $attributes['filters'], ); @@ -54,9 +58,10 @@ public static function from(array $attributes): self */ public function toArray(): array { + // @phpstan-ignore-next-line return [ 'filters' => array_map( - static fn (FileSearchComparisonFilter $filter): array => $filter->toArray(), + static fn (FileSearchComparisonFilter|FileSearchCompoundFilter $filter): array => $filter->toArray(), $this->filters, ), 'type' => $this->type, diff --git a/tests/Fixtures/Responses.php b/tests/Fixtures/Responses.php index e50e0cbf..32bd5558 100644 --- a/tests/Fixtures/Responses.php +++ b/tests/Fixtures/Responses.php @@ -690,6 +690,45 @@ function toolFileSearch(): array ]; } +/** + * @return array + */ +function toolFileSearchNestedFilters(): array +{ + return [ + 'type' => 'file_search', + 'filters' => [ + 'type' => 'and', + 'filters' => [ + [ + 'type' => 'and', + 'filters' => [ + [ + 'type' => 'ne', + 'key' => 'state', + 'value' => 'ks', + ], + [ + 'type' => 'ne', + 'key' => 'state', + 'value' => 'mo', + ], + ], + ], + ], + ], + 'max_num_results' => 5, + 'ranking_options' => [ + 'ranker' => 'auto', + 'score_threshold' => 0.1, + ], + 'vector_store_ids' => [ + 'vector_store_id_1', + 'vector_store_id_2', + ], + ]; +} + /** * @return resource */ diff --git a/tests/Responses/Responses/Tool/FileSearchTool.php b/tests/Responses/Responses/Tool/FileSearchTool.php index 3637074d..c551eb37 100644 --- a/tests/Responses/Responses/Tool/FileSearchTool.php +++ b/tests/Responses/Responses/Tool/FileSearchTool.php @@ -1,6 +1,7 @@ filters->toBeNull(); }); +test('from complex nested filters', function () { + $response = FileSearchTool::from(toolFileSearchNestedFilters()); + + expect($response) + ->toBeInstanceOf(FileSearchTool::class) + ->filters->toBeInstanceOf(FileSearchCompoundFilter::class) + ->filters->filters->toBeArray() + ->and($response->filters->filters[0]) + ->toBeInstanceOf(FileSearchCompoundFilter::class) + ->filters->toBeArray() + ->and($response->filters->filters[0]->filters[0]) + ->toBeInstanceOf(FileSearchComparisonFilter::class) + ->and($response->filters->filters[0]->filters[1]) + ->toBeInstanceOf(FileSearchComparisonFilter::class); +}); + test('from results', function () { $response = FileSearchTool::from(toolFileSearch()); From a00d7756dd29bddf25fcead7ff1964be7a4a52fe Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Wed, 3 Sep 2025 06:57:17 -0400 Subject: [PATCH 08/11] refactor(meta): unify response types into Actions (#671) * refactor(meta): unify response types into Actions * chore: unify types on output text for less duplication * chore: unify types on tool choice for less duplication * chore: unify types on tools for less duplication * chore: unify types on output for less duplication * chore: restore longer type for proper ide usage * chore: remove unused type --- src/Actions/Responses/ItemObjects.php | 68 +++++++++++++++ src/Actions/Responses/OutputObjects.php | 60 ++++++++++++++ src/Actions/Responses/OutputText.php | 35 ++++++++ src/Actions/Responses/ToolChoiceObjects.php | 32 +++++++ src/Actions/Responses/ToolObjects.php | 48 +++++++++++ src/Responses/Responses/CreateResponse.php | 86 +++---------------- src/Responses/Responses/ListInputItems.php | 37 +-------- src/Responses/Responses/RetrieveResponse.php | 87 +++----------------- tests/Arch.php | 1 + 9 files changed, 272 insertions(+), 182 deletions(-) create mode 100644 src/Actions/Responses/ItemObjects.php create mode 100644 src/Actions/Responses/OutputObjects.php create mode 100644 src/Actions/Responses/OutputText.php create mode 100644 src/Actions/Responses/ToolChoiceObjects.php create mode 100644 src/Actions/Responses/ToolObjects.php diff --git a/src/Actions/Responses/ItemObjects.php b/src/Actions/Responses/ItemObjects.php new file mode 100644 index 00000000..f31e5657 --- /dev/null +++ b/src/Actions/Responses/ItemObjects.php @@ -0,0 +1,68 @@ + + * @phpstan-type ResponseItemObjectReturnType array + */ +final class ItemObjects +{ + /** + * @param ResponseItemObjectTypes $outputItems + * @return ResponseItemObjectReturnType + */ + public static function parse(array $outputItems): array + { + return array_map( + fn (array $item): InputMessage|ComputerToolCallOutput|FunctionToolCallOutput|OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall => 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), + 'reasoning' => OutputReasoning::from($item), + 'mcp_list_tools' => OutputMcpListTools::from($item), + 'mcp_approval_request' => OutputMcpApprovalRequest::from($item), + 'mcp_call' => OutputMcpCall::from($item), + 'image_generation_call' => OutputImageGenerationToolCall::from($item), + 'code_interpreter_call' => OutputCodeInterpreterToolCall::from($item), + }, + $outputItems, + ); + } +} diff --git a/src/Actions/Responses/OutputObjects.php b/src/Actions/Responses/OutputObjects.php new file mode 100644 index 00000000..8d30cb87 --- /dev/null +++ b/src/Actions/Responses/OutputObjects.php @@ -0,0 +1,60 @@ + + * @phpstan-type ResponseOutputObjectReturnType array + */ +final class OutputObjects +{ + /** + * @param ResponseOutputObjectTypes $outputItems + * @return ResponseOutputObjectReturnType + */ + public static function parse(array $outputItems): array + { + return array_map( + fn (array $item): OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall => match ($item['type']) { + 'message' => OutputMessage::from($item), + 'file_search_call' => OutputFileSearchToolCall::from($item), + 'function_call' => OutputFunctionToolCall::from($item), + 'web_search_call' => OutputWebSearchToolCall::from($item), + 'computer_call' => OutputComputerToolCall::from($item), + 'reasoning' => OutputReasoning::from($item), + 'mcp_list_tools' => OutputMcpListTools::from($item), + 'mcp_approval_request' => OutputMcpApprovalRequest::from($item), + 'mcp_call' => OutputMcpCall::from($item), + 'image_generation_call' => OutputImageGenerationToolCall::from($item), + 'code_interpreter_call' => OutputCodeInterpreterToolCall::from($item), + }, + $outputItems, + ); + } +} diff --git a/src/Actions/Responses/OutputText.php b/src/Actions/Responses/OutputText.php new file mode 100644 index 00000000..e70fd889 --- /dev/null +++ b/src/Actions/Responses/OutputText.php @@ -0,0 +1,35 @@ +content as $content) { + if ($content instanceof OutputMessageContentOutputText) { + $texts[] = $content->text; + } + } + } + } + + return empty($texts) ? null : implode(' ', $texts); + } +} diff --git a/src/Actions/Responses/ToolChoiceObjects.php b/src/Actions/Responses/ToolChoiceObjects.php new file mode 100644 index 00000000..9dc52cf7 --- /dev/null +++ b/src/Actions/Responses/ToolChoiceObjects.php @@ -0,0 +1,32 @@ + HostedToolChoice::from($toolChoice), + 'function' => FunctionToolChoice::from($toolChoice), + } + : $toolChoice; + } +} diff --git a/src/Actions/Responses/ToolObjects.php b/src/Actions/Responses/ToolObjects.php new file mode 100644 index 00000000..774d04cc --- /dev/null +++ b/src/Actions/Responses/ToolObjects.php @@ -0,0 +1,48 @@ + + * @phpstan-type ResponseToolObjectReturnType array + */ +final class ToolObjects +{ + /** + * @param ResponseToolObjectTypes $toolItems + * @return ResponseToolObjectReturnType + */ + public static function parse(array $toolItems): array + { + return array_map( + fn (array $tool): ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool => match ($tool['type']) { + 'file_search' => FileSearchTool::from($tool), + 'web_search', '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), + 'mcp' => RemoteMcpTool::from($tool), + 'code_interpreter' => CodeInterpreterTool::from($tool), + }, + $toolItems, + ); + } +} diff --git a/src/Responses/Responses/CreateResponse.php b/src/Responses/Responses/CreateResponse.php index 94eef4ac..239997a1 100644 --- a/src/Responses/Responses/CreateResponse.php +++ b/src/Responses/Responses/CreateResponse.php @@ -4,6 +4,10 @@ namespace OpenAI\Responses\Responses; +use OpenAI\Actions\Responses\OutputObjects; +use OpenAI\Actions\Responses\OutputText; +use OpenAI\Actions\Responses\ToolChoiceObjects; +use OpenAI\Actions\Responses\ToolObjects; use OpenAI\Contracts\ResponseContract; use OpenAI\Contracts\ResponseHasMetaInformationContract; use OpenAI\Responses\Concerns\ArrayAccessible; @@ -18,7 +22,6 @@ use OpenAI\Responses\Responses\Output\OutputMcpCall; use OpenAI\Responses\Responses\Output\OutputMcpListTools; 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\CodeInterpreterTool; @@ -34,37 +37,17 @@ /** * @phpstan-import-type ResponseFormatType from CreateResponseFormat - * @phpstan-import-type OutputComputerToolCallType from OutputComputerToolCall - * @phpstan-import-type OutputFileSearchToolCallType from OutputFileSearchToolCall - * @phpstan-import-type OutputFunctionToolCallType from OutputFunctionToolCall - * @phpstan-import-type OutputMessageType from OutputMessage - * @phpstan-import-type OutputReasoningType from OutputReasoning - * @phpstan-import-type OutputWebSearchToolCallType from OutputWebSearchToolCall - * @phpstan-import-type OutputMcpListToolsType from OutputMcpListTools - * @phpstan-import-type OutputMcpApprovalRequestType from OutputMcpApprovalRequest - * @phpstan-import-type OutputMcpCallType from OutputMcpCall - * @phpstan-import-type OutputImageGenerationToolCallType from OutputImageGenerationToolCall - * @phpstan-import-type OutputCodeInterpreterToolCallType from OutputCodeInterpreterToolCall - * @phpstan-import-type ComputerUseToolType from ComputerUseTool - * @phpstan-import-type FileSearchToolType from FileSearchTool - * @phpstan-import-type ImageGenerationToolType from ImageGenerationTool - * @phpstan-import-type RemoteMcpToolType from RemoteMcpTool - * @phpstan-import-type FunctionToolType from FunctionTool - * @phpstan-import-type WebSearchToolType from WebSearchTool - * @phpstan-import-type CodeInterpreterToolType from CodeInterpreterTool * @phpstan-import-type ErrorType from GenericResponseError * @phpstan-import-type IncompleteDetailsType from CreateResponseIncompleteDetails * @phpstan-import-type UsageType from CreateResponseUsage - * @phpstan-import-type FunctionToolChoiceType from FunctionToolChoice - * @phpstan-import-type HostedToolChoiceType from HostedToolChoice * @phpstan-import-type ReasoningType from CreateResponseReasoning * @phpstan-import-type ReferencePromptObjectType from ReferencePromptObject + * @phpstan-import-type ResponseOutputObjectTypes from OutputObjects + * @phpstan-import-type ResponseToolChoiceTypes from ToolChoiceObjects + * @phpstan-import-type ResponseToolObjectTypes from ToolObjects * * @phpstan-type InstructionsType array|string|null - * @phpstan-type ToolChoiceType 'none'|'auto'|'required'|FunctionToolChoiceType|HostedToolChoiceType - * @phpstan-type ToolsType array - * @phpstan-type OutputType array - * @phpstan-type CreateResponseType array{id: string, background?: bool|null, object: 'response', created_at: int, status: 'completed'|'failed'|'in_progress'|'incomplete', error: ErrorType|null, incomplete_details: IncompleteDetailsType|null, instructions: InstructionsType, max_output_tokens: int|null, max_tool_calls?: int|null, model: string, output: OutputType, output_text: string|null, parallel_tool_calls: bool, previous_response_id: string|null, prompt: ReferencePromptObjectType|null, prompt_cache_key?: string|null, reasoning: ReasoningType|null, safety_identifier?: string|null, service_tier?: string|null, store?: bool|null, temperature: float|null, text?: ResponseFormatType|null, tool_choice: ToolChoiceType, tools: ToolsType, top_logprobs?: int|null, top_p: float|null, truncation: 'auto'|'disabled'|null, usage: UsageType|null, user: string|null, verbosity: string|null, metadata: array|null} + * @phpstan-type CreateResponseType array{id: string, background?: bool|null, object: 'response', created_at: int, status: 'completed'|'failed'|'in_progress'|'incomplete', error: ErrorType|null, incomplete_details: IncompleteDetailsType|null, instructions: InstructionsType, max_output_tokens: int|null, max_tool_calls?: int|null, model: string, output: ResponseOutputObjectTypes, output_text: string|null, parallel_tool_calls: bool, previous_response_id: string|null, prompt: ReferencePromptObjectType|null, prompt_cache_key?: string|null, reasoning: ReasoningType|null, safety_identifier?: string|null, service_tier?: string|null, store?: bool|null, temperature: float|null, text?: ResponseFormatType|null, tool_choice: ResponseToolChoiceTypes, tools: ResponseToolObjectTypes, top_logprobs?: int|null, top_p: float|null, truncation: 'auto'|'disabled'|null, usage: UsageType|null, user: string|null, verbosity: string|null, metadata: array|null} * * @implements ResponseContract */ @@ -128,54 +111,9 @@ private function __construct( */ public static function from(array $attributes, MetaInformation $meta): self { - $output = array_map( - fn (array $output): OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall => 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), - 'mcp_list_tools' => OutputMcpListTools::from($output), - 'mcp_approval_request' => OutputMcpApprovalRequest::from($output), - 'mcp_call' => OutputMcpCall::from($output), - 'image_generation_call' => OutputImageGenerationToolCall::from($output), - 'code_interpreter_call' => OutputCodeInterpreterToolCall::from($output), - }, - $attributes['output'], - ); - - $toolChoice = is_array($attributes['tool_choice']) - ? match ($attributes['tool_choice']['type']) { - 'file_search', 'web_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|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool => match ($tool['type']) { - 'file_search' => FileSearchTool::from($tool), - 'web_search', '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), - 'mcp' => RemoteMcpTool::from($tool), - 'code_interpreter' => CodeInterpreterTool::from($tool), - }, - $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; - } - } - } - } + $output = OutputObjects::parse($attributes['output']); + $toolChoice = ToolChoiceObjects::parse($attributes['tool_choice']); + $tools = ToolObjects::parse($attributes['tools']); return new self( id: $attributes['id'], @@ -194,7 +132,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), + outputText: OutputText::parse($output), parallelToolCalls: $attributes['parallel_tool_calls'], previousResponseId: $attributes['previous_response_id'], prompt: isset($attributes['prompt']) diff --git a/src/Responses/Responses/ListInputItems.php b/src/Responses/Responses/ListInputItems.php index 018c2a98..adeaa860 100644 --- a/src/Responses/Responses/ListInputItems.php +++ b/src/Responses/Responses/ListInputItems.php @@ -4,6 +4,7 @@ namespace OpenAI\Responses\Responses; +use OpenAI\Actions\Responses\ItemObjects; use OpenAI\Contracts\ResponseContract; use OpenAI\Contracts\ResponseHasMetaInformationContract; use OpenAI\Responses\Concerns\ArrayAccessible; @@ -26,22 +27,9 @@ use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @phpstan-import-type InputMessageType from InputMessage - * @phpstan-import-type OutputMessageType from OutputMessage - * @phpstan-import-type OutputFileSearchToolCallType from OutputFileSearchToolCall - * @phpstan-import-type OutputComputerToolCallType from OutputComputerToolCall - * @phpstan-import-type ComputerToolCallOutputType from ComputerToolCallOutput - * @phpstan-import-type OutputWebSearchToolCallType from OutputWebSearchToolCall - * @phpstan-import-type OutputFunctionToolCallType from OutputFunctionToolCall - * @phpstan-import-type FunctionToolCallOutputType from FunctionToolCallOutput - * @phpstan-import-type OutputReasoningType from OutputReasoning - * @phpstan-import-type OutputMcpListToolsType from OutputMcpListTools - * @phpstan-import-type OutputMcpApprovalRequestType from OutputMcpApprovalRequest - * @phpstan-import-type OutputMcpCallType from OutputMcpCall - * @phpstan-import-type OutputImageGenerationToolCallType from OutputImageGenerationToolCall - * @phpstan-import-type OutputCodeInterpreterToolCallType from OutputCodeInterpreterToolCall + * @phpstan-import-type ResponseItemObjectTypes from ItemObjects * - * @phpstan-type ListInputItemsType array{data: array, first_id: string, has_more: bool, last_id: string, object: 'list'} + * @phpstan-type ListInputItemsType array{data: ResponseItemObjectTypes, first_id: string, has_more: bool, last_id: string, object: 'list'} * * @implements ResponseContract */ @@ -71,24 +59,7 @@ private function __construct( */ public static function from(array $attributes, MetaInformation $meta): self { - $data = array_map( - fn (array $item): InputMessage|OutputMessage|OutputFileSearchToolCall|OutputComputerToolCall|ComputerToolCallOutput|OutputWebSearchToolCall|OutputFunctionToolCall|FunctionToolCallOutput|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall => 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), - 'reasoning' => OutputReasoning::from($item), - 'mcp_list_tools' => OutputMcpListTools::from($item), - 'mcp_approval_request' => OutputMcpApprovalRequest::from($item), - 'mcp_call' => OutputMcpCall::from($item), - 'image_generation_call' => OutputImageGenerationToolCall::from($item), - 'code_interpreter_call' => OutputCodeInterpreterToolCall::from($item), - }, - $attributes['data'], - ); + $data = ItemObjects::parse($attributes['data']); return new self( object: $attributes['object'], diff --git a/src/Responses/Responses/RetrieveResponse.php b/src/Responses/Responses/RetrieveResponse.php index bdc117dd..01243565 100644 --- a/src/Responses/Responses/RetrieveResponse.php +++ b/src/Responses/Responses/RetrieveResponse.php @@ -4,6 +4,10 @@ namespace OpenAI\Responses\Responses; +use OpenAI\Actions\Responses\OutputObjects; +use OpenAI\Actions\Responses\OutputText; +use OpenAI\Actions\Responses\ToolChoiceObjects; +use OpenAI\Actions\Responses\ToolObjects; use OpenAI\Contracts\ResponseContract; use OpenAI\Contracts\ResponseHasMetaInformationContract; use OpenAI\Responses\Concerns\ArrayAccessible; @@ -18,7 +22,6 @@ use OpenAI\Responses\Responses\Output\OutputMcpCall; use OpenAI\Responses\Responses\Output\OutputMcpListTools; 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\CodeInterpreterTool; @@ -34,38 +37,17 @@ /** * @phpstan-import-type ResponseFormatType from CreateResponseFormat - * @phpstan-import-type OutputComputerToolCallType from OutputComputerToolCall - * @phpstan-import-type OutputFileSearchToolCallType from OutputFileSearchToolCall - * @phpstan-import-type OutputFunctionToolCallType from OutputFunctionToolCall - * @phpstan-import-type OutputMessageType from OutputMessage - * @phpstan-import-type OutputReasoningType from OutputReasoning - * @phpstan-import-type OutputWebSearchToolCallType from OutputWebSearchToolCall - * @phpstan-import-type OutputImageGenerationToolCallType from OutputImageGenerationToolCall - * @phpstan-import-type OutputMcpListToolsType from OutputMcpListTools - * @phpstan-import-type OutputMcpApprovalRequestType from OutputMcpApprovalRequest - * @phpstan-import-type OutputMcpCallType from OutputMcpCall - * @phpstan-import-type OutputImageGenerationToolCallType from OutputImageGenerationToolCall - * @phpstan-import-type OutputCodeInterpreterToolCallType from OutputCodeInterpreterToolCall - * @phpstan-import-type ComputerUseToolType from ComputerUseTool - * @phpstan-import-type FileSearchToolType from FileSearchTool - * @phpstan-import-type ImageGenerationToolType from ImageGenerationTool - * @phpstan-import-type RemoteMcpToolType from RemoteMcpTool - * @phpstan-import-type FunctionToolType from FunctionTool - * @phpstan-import-type WebSearchToolType from WebSearchTool - * @phpstan-import-type CodeInterpreterToolType from CodeInterpreterTool * @phpstan-import-type ErrorType from GenericResponseError * @phpstan-import-type IncompleteDetailsType from CreateResponseIncompleteDetails * @phpstan-import-type UsageType from CreateResponseUsage - * @phpstan-import-type FunctionToolChoiceType from FunctionToolChoice - * @phpstan-import-type HostedToolChoiceType from HostedToolChoice * @phpstan-import-type ReasoningType from CreateResponseReasoning * @phpstan-import-type ReferencePromptObjectType from ReferencePromptObject + * @phpstan-import-type ResponseOutputObjectTypes from OutputObjects + * @phpstan-import-type ResponseToolChoiceTypes from ToolChoiceObjects + * @phpstan-import-type ResponseToolObjectTypes from ToolObjects * * @phpstan-type InstructionsType array|string|null - * @phpstan-type ToolChoiceType 'none'|'auto'|'required'|FunctionToolChoiceType|HostedToolChoiceType - * @phpstan-type ToolsType array - * @phpstan-type OutputType array - * @phpstan-type RetrieveResponseType array{id: string, background?: bool|null, object: 'response', created_at: int, status: 'completed'|'failed'|'in_progress'|'incomplete', error: ErrorType|null, incomplete_details: IncompleteDetailsType|null, instructions: InstructionsType, max_output_tokens: int|null, max_tool_calls?: int|null, model: string, output: OutputType, output_text: string|null, parallel_tool_calls: bool, previous_response_id: string|null, prompt: ReferencePromptObjectType|null, prompt_cache_key?: string|null, reasoning: ReasoningType|null, safety_identifier?: string|null, service_tier?: string|null, store: bool, temperature: float|null, text: ResponseFormatType, tool_choice: ToolChoiceType, tools: ToolsType, top_logprobs?: int|null, top_p: float|null, truncation: 'auto'|'disabled'|null, usage: UsageType|null, user: string|null, verbosity: string|null, metadata: array|null} + * @phpstan-type RetrieveResponseType array{id: string, background?: bool|null, object: 'response', created_at: int, status: 'completed'|'failed'|'in_progress'|'incomplete', error: ErrorType|null, incomplete_details: IncompleteDetailsType|null, instructions: InstructionsType, max_output_tokens: int|null, max_tool_calls?: int|null, model: string, output: ResponseOutputObjectTypes, output_text: string|null, parallel_tool_calls: bool, previous_response_id: string|null, prompt: ReferencePromptObjectType|null, prompt_cache_key?: string|null, reasoning: ReasoningType|null, safety_identifier?: string|null, service_tier?: string|null, store: bool, temperature: float|null, text: ResponseFormatType, tool_choice: ResponseToolChoiceTypes, tools: ResponseToolObjectTypes, top_logprobs?: int|null, top_p: float|null, truncation: 'auto'|'disabled'|null, usage: UsageType|null, user: string|null, verbosity: string|null, metadata: array|null} * * @implements ResponseContract */ @@ -129,54 +111,9 @@ private function __construct( */ public static function from(array $attributes, MetaInformation $meta): self { - $output = array_map( - fn (array $output): OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall => 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), - 'mcp_list_tools' => OutputMcpListTools::from($output), - 'mcp_approval_request' => OutputMcpApprovalRequest::from($output), - 'mcp_call' => OutputMcpCall::from($output), - 'image_generation_call' => OutputImageGenerationToolCall::from($output), - 'code_interpreter_call' => OutputCodeInterpreterToolCall::from($output), - }, - $attributes['output'], - ); - - $toolChoice = is_array($attributes['tool_choice']) - ? match ($attributes['tool_choice']['type']) { - 'file_search', 'web_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|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool => match ($tool['type']) { - 'file_search' => FileSearchTool::from($tool), - 'web_search', '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), - 'mcp' => RemoteMcpTool::from($tool), - 'code_interpreter' => CodeInterpreterTool::from($tool), - }, - $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; - } - } - } - } + $output = OutputObjects::parse($attributes['output']); + $toolChoice = ToolChoiceObjects::parse($attributes['tool_choice']); + $tools = ToolObjects::parse($attributes['tools']); return new self( id: $attributes['id'], @@ -195,7 +132,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), + outputText: OutputText::parse($output), parallelToolCalls: $attributes['parallel_tool_calls'], previousResponseId: $attributes['previous_response_id'], prompt: isset($attributes['prompt']) diff --git a/tests/Arch.php b/tests/Arch.php index b63c7a6d..c6836e4a 100644 --- a/tests/Arch.php +++ b/tests/Arch.php @@ -31,6 +31,7 @@ test('responses')->expect('OpenAI\Responses')->toOnlyUse([ 'Http\Discovery\Psr17Factory', + 'OpenAI\Actions', 'OpenAI\Enums', 'OpenAI\Exceptions\ErrorException', 'OpenAI\Exceptions\UnknownEventException', From 166ab20cb13c782bb0f9c676e744491a99e1a8a0 Mon Sep 17 00:00:00 2001 From: Louis Bels Date: Wed, 3 Sep 2025 18:22:19 +0200 Subject: [PATCH 09/11] fix(OpenAI): Add support for reasoning text streaming events (#673) * Add support for `response.reasoning_text.delta` and `response.reasoning_text.done` events. * Remove `type` attribute from `ReasoningTextDelta` and `ReasoningTextDone` responses. --- .../Responses/CreateStreamedResponse.php | 6 +- .../Streaming/ReasoningTextDelta.php | 66 +++++++++++++++++++ .../Responses/Streaming/ReasoningTextDone.php | 66 +++++++++++++++++++ tests/Fixtures/Responses.php | 10 +++ .../Streams/ResponseReasoningTextDelta.txt | 1 + .../Streams/ResponseReasoningTextDone.txt | 1 + .../Responses/CreateStreamedResponse.php | 30 +++++++++ 7 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 src/Responses/Responses/Streaming/ReasoningTextDelta.php create mode 100644 src/Responses/Responses/Streaming/ReasoningTextDone.php create mode 100644 tests/Fixtures/Streams/ResponseReasoningTextDelta.txt create mode 100644 tests/Fixtures/Streams/ResponseReasoningTextDone.txt diff --git a/src/Responses/Responses/CreateStreamedResponse.php b/src/Responses/Responses/CreateStreamedResponse.php index fdd8791b..99efe66e 100644 --- a/src/Responses/Responses/CreateStreamedResponse.php +++ b/src/Responses/Responses/CreateStreamedResponse.php @@ -29,6 +29,8 @@ use OpenAI\Responses\Responses\Streaming\ReasoningSummaryPart; use OpenAI\Responses\Responses\Streaming\ReasoningSummaryTextDelta; use OpenAI\Responses\Responses\Streaming\ReasoningSummaryTextDone; +use OpenAI\Responses\Responses\Streaming\ReasoningTextDelta; +use OpenAI\Responses\Responses\Streaming\ReasoningTextDone; use OpenAI\Responses\Responses\Streaming\RefusalDelta; use OpenAI\Responses\Responses\Streaming\RefusalDone; use OpenAI\Responses\Responses\Streaming\WebSearchCall; @@ -50,7 +52,7 @@ final class CreateStreamedResponse implements ResponseContract private function __construct( public readonly string $event, - public readonly CreateResponse|OutputItem|ContentPart|OutputTextDelta|OutputTextAnnotationAdded|OutputTextDone|RefusalDelta|RefusalDone|FunctionCallArgumentsDelta|FunctionCallArgumentsDone|FileSearchCall|WebSearchCall|CodeInterpreterCall|CodeInterpreterCodeDelta|CodeInterpreterCodeDone|ReasoningSummaryPart|ReasoningSummaryTextDelta|ReasoningSummaryTextDone|McpListTools|McpListToolsInProgress|McpCall|McpCallArgumentsDelta|McpCallArgumentsDone|ImageGenerationPart|ImageGenerationPartialImage|Error $response, + public readonly CreateResponse|OutputItem|ContentPart|OutputTextDelta|OutputTextAnnotationAdded|OutputTextDone|RefusalDelta|RefusalDone|FunctionCallArgumentsDelta|FunctionCallArgumentsDone|FileSearchCall|WebSearchCall|CodeInterpreterCall|CodeInterpreterCodeDelta|CodeInterpreterCodeDone|ReasoningSummaryPart|ReasoningSummaryTextDelta|ReasoningSummaryTextDone|ReasoningTextDelta|ReasoningTextDone|McpListTools|McpListToolsInProgress|McpCall|McpCallArgumentsDelta|McpCallArgumentsDone|ImageGenerationPart|ImageGenerationPartialImage|Error $response, ) {} /** @@ -95,6 +97,8 @@ public static function from(array $attributes): self '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 + 'response.reasoning_text.delta' => ReasoningTextDelta::from($attributes, $meta), // @phpstan-ignore-line + 'response.reasoning_text.done' => ReasoningTextDone::from($attributes, $meta), // @phpstan-ignore-line 'response.mcp_list_tools.in_progress' => McpListToolsInProgress::from($attributes, $meta), // @phpstan-ignore-line 'response.mcp_list_tools.failed', 'response.mcp_list_tools.completed' => McpListTools::from($attributes, $meta), // @phpstan-ignore-line diff --git a/src/Responses/Responses/Streaming/ReasoningTextDelta.php b/src/Responses/Responses/Streaming/ReasoningTextDelta.php new file mode 100644 index 00000000..92f3098d --- /dev/null +++ b/src/Responses/Responses/Streaming/ReasoningTextDelta.php @@ -0,0 +1,66 @@ + + */ +final class ReasoningTextDelta 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, + public readonly int $sequenceNumber, + private readonly MetaInformation $meta, + ) {} + + /** + * @param ReasoningTextDeltaType $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'], + sequenceNumber: $attributes['sequence_number'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'content_index' => $this->contentIndex, + 'delta' => $this->delta, + 'item_id' => $this->itemId, + 'output_index' => $this->outputIndex, + 'sequence_number' => $this->sequenceNumber, + ]; + } +} diff --git a/src/Responses/Responses/Streaming/ReasoningTextDone.php b/src/Responses/Responses/Streaming/ReasoningTextDone.php new file mode 100644 index 00000000..0f5623b7 --- /dev/null +++ b/src/Responses/Responses/Streaming/ReasoningTextDone.php @@ -0,0 +1,66 @@ + + */ +final class ReasoningTextDone 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 int $sequenceNumber, + public readonly string $text, + private readonly MetaInformation $meta, + ) {} + + /** + * @param ReasoningTextDoneType $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'], + sequenceNumber: $attributes['sequence_number'], + text: $attributes['text'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'content_index' => $this->contentIndex, + 'item_id' => $this->itemId, + 'output_index' => $this->outputIndex, + 'sequence_number' => $this->sequenceNumber, + 'text' => $this->text, + ]; + } +} diff --git a/tests/Fixtures/Responses.php b/tests/Fixtures/Responses.php index 32bd5558..c2e7e7d9 100644 --- a/tests/Fixtures/Responses.php +++ b/tests/Fixtures/Responses.php @@ -760,3 +760,13 @@ function responseCompletionSteamCreatedEvent() { return fopen(__DIR__.'/Streams/ResponseCreatedResponse.txt', 'r'); } + +function responseReasoningTextDeltaEvent() +{ + return fopen(__DIR__.'/Streams/ResponseReasoningTextDelta.txt', 'r'); +} + +function responseReasoningTextDoneEvent() +{ + return fopen(__DIR__.'/Streams/ResponseReasoningTextDone.txt', 'r'); +} diff --git a/tests/Fixtures/Streams/ResponseReasoningTextDelta.txt b/tests/Fixtures/Streams/ResponseReasoningTextDelta.txt new file mode 100644 index 00000000..a5580512 --- /dev/null +++ b/tests/Fixtures/Streams/ResponseReasoningTextDelta.txt @@ -0,0 +1 @@ +data: {"type":"response.reasoning_text.delta","content_index":0,"delta":"Let me analyze","item_id":"item_123","output_index":0,"sequence_number":5} diff --git a/tests/Fixtures/Streams/ResponseReasoningTextDone.txt b/tests/Fixtures/Streams/ResponseReasoningTextDone.txt new file mode 100644 index 00000000..92ac8e3d --- /dev/null +++ b/tests/Fixtures/Streams/ResponseReasoningTextDone.txt @@ -0,0 +1 @@ +data: {"type":"response.reasoning_text.done","content_index":0,"item_id":"item_123","output_index":0,"sequence_number":10,"text":"Let me analyze this problem step by step to provide the best solution."} \ No newline at end of file diff --git a/tests/Responses/Responses/CreateStreamedResponse.php b/tests/Responses/Responses/CreateStreamedResponse.php index b718ad41..49d6f56f 100644 --- a/tests/Responses/Responses/CreateStreamedResponse.php +++ b/tests/Responses/Responses/CreateStreamedResponse.php @@ -2,6 +2,8 @@ use OpenAI\Responses\Responses\CreateResponse; use OpenAI\Responses\Responses\CreateStreamedResponse; +use OpenAI\Responses\Responses\Streaming\ReasoningTextDelta; +use OpenAI\Responses\Responses\Streaming\ReasoningTextDone; test('fake', function () { $response = CreateStreamedResponse::fake(); @@ -20,3 +22,31 @@ ->response->toBeInstanceOf(CreateResponse::class) ->response->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); }); + +test('reasoning text delta event', function () { + $response = CreateStreamedResponse::fake(responseReasoningTextDeltaEvent()); + + expect($response->getIterator()->current()) + ->toBeInstanceOf(CreateStreamedResponse::class) + ->event->toBe('response.reasoning_text.delta') + ->response->toBeInstanceOf(ReasoningTextDelta::class) + ->response->delta->toBe('Let me analyze') + ->response->itemId->toBe('item_123') + ->response->outputIndex->toBe(0) + ->response->contentIndex->toBe(0) + ->response->sequenceNumber->toBe(5); +}); + +test('reasoning text done event', function () { + $response = CreateStreamedResponse::fake(responseReasoningTextDoneEvent()); + + expect($response->getIterator()->current()) + ->toBeInstanceOf(CreateStreamedResponse::class) + ->event->toBe('response.reasoning_text.done') + ->response->toBeInstanceOf(ReasoningTextDone::class) + ->response->text->toBe('Let me analyze this problem step by step to provide the best solution.') + ->response->itemId->toBe('item_123') + ->response->outputIndex->toBe(0) + ->response->contentIndex->toBe(0) + ->response->sequenceNumber->toBe(10); +}); From 2e37dac67a7b3a38b5b50d894a171d2920661f09 Mon Sep 17 00:00:00 2001 From: Ruslan Polutsygan Date: Thu, 4 Sep 2025 17:16:58 +0300 Subject: [PATCH 10/11] fix(meta): Remove extra spaces from json keys in ThreadRunStreamResponseFixture.txt (#674) * remove extra spaces from json keys in ThreadRunStreamResponseFixture.txt * fix typo in fixture model name "gpt-4 o" => "gpt-4o" --- .../Fixtures/Threads/Runs/ThreadRunStreamResponseFixture.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Testing/Responses/Fixtures/Threads/Runs/ThreadRunStreamResponseFixture.txt b/src/Testing/Responses/Fixtures/Threads/Runs/ThreadRunStreamResponseFixture.txt index 74781b70..6f3d2852 100644 --- a/src/Testing/Responses/Fixtures/Threads/Runs/ThreadRunStreamResponseFixture.txt +++ b/src/Testing/Responses/Fixtures/Threads/Runs/ThreadRunStreamResponseFixture.txt @@ -1,7 +1,7 @@ event: thread.created data: {"id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","object":"thread","created_at":1720104398,"metadata":{"user":"John Doe"},"tool_resources":{"code_interpreter":{"file_ids":[]}}} event: thread.run.created -data: {"id":"run_s1X8yAjuUBlwhGrqiahzfnH7","object":"thread.run","created_at":1720104398,"assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","status":"queued","started_at ":null,"expires_at ":1720104998,"cancelled_at ":null,"failed_at ":null,"completed_at ":null,"required_action ":null,"last_error ":null,"model":"gpt-4 o","instructions":"You are a very useful assistant","tools":[{"type":"code_interpreter"},{"type":"file_search","file_search":{"max_num_results":50}},{"type":"function","function":{"name":"get_weather","description":"Determine weather in my location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The city and state e.g. San Francisco, CA"},"unit":{"type":"string","enum":["c","f"]}},"required":["location"]}}}],"tool_resources":{"code_interpreter":{"file_ids":[]}},"metadata":{"user":"John Doe"},"temperature":0.7,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true} +data: {"id":"run_s1X8yAjuUBlwhGrqiahzfnH7","object":"thread.run","created_at":1720104398,"assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","status":"queued","started_at":null,"expires_at":1720104998,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":"You are a very useful assistant","tools":[{"type":"code_interpreter"},{"type":"file_search","file_search":{"max_num_results":50}},{"type":"function","function":{"name":"get_weather","description":"Determine weather in my location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The city and state e.g. San Francisco, CA"},"unit":{"type":"string","enum":["c","f"]}},"required":["location"]}}}],"tool_resources":{"code_interpreter":{"file_ids":[]}},"metadata":{"user":"John Doe"},"temperature":0.7,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true} event: thread.run.queued data: {"id":"run_s1X8yAjuUBlwhGrqiahzfnH7","object":"thread.run","created_at":1720104398,"assistant_id":"asst_JA9Pc6eQ744nbec10slSz5BU","thread_id":"thread_sSbvUX4J1FqlUZBv6BaBbOj4","status":"queued","started_at":null,"expires_at":1720104998,"cancelled_at":null,"failed_at":null,"completed_at":null,"required_action":null,"last_error":null,"model":"gpt-4o","instructions":"You are a very useful assistant.","tools":[{"type":"code_interpreter"},{"type":"file_search","file_search":{"max_num_results":50}},{"type":"function","function":{"name":"get_weather","description":"Determine weather in my location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The city and state e.g. San Francisco, CA"},"unit":{"type":"string","enum":["c","f"]}},"required":["location"]}}}],"tool_resources":{"code_interpreter":{"file_ids":[]}},"metadata":{"user":"John Doe"},"temperature":0.7,"top_p":1.0,"max_completion_tokens":null,"max_prompt_tokens":null,"truncation_strategy":{"type":"auto","last_messages":null},"incomplete_details":null,"usage":null,"response_format":"auto","tool_choice":"auto","parallel_tool_calls":true} event: thread.run.in_progress From 7a59e4d896d83f8c923e0b3a5ebae0a5cddad2d6 Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Thu, 4 Sep 2025 10:32:16 -0400 Subject: [PATCH 11/11] release: v0.16.1 (#676) --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51d492cb..6a7332cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/) and this project adheres to [Semantic Versioning](https://semver.org/). +# v0.16.1 (2025-09-04) +### Added + +* Add `sequence_number` support to `OutputTextDelta` ([#664](https://github.com/openai-php/client/pull/664)) +* Add `web-search` tool option ([#665](https://github.com/openai-php/client/pull/665)) +* Add support for reasoning text streaming events ([#673](https://github.com/openai-php/client/pull/673)) + +### Fixed + +* Handle optional attributes in CreateResponse ([#662](https://github.com/openai-php/client/pull/662)) +* Handle null require_approval in Response API ([#669](https://github.com/openai-php/client/pull/669)) +* Support mcp error objects in response api ([#661](https://github.com/openai-php/client/pull/661)) +* Support nested file search properties ([#670](https://github.com/openai-php/client/pull/670)) +* Add missing properties to ListInputItems call ([#668](https://github.com/openai-php/client/pull/668)) +* Remove extra spaces from json keys in ThreadRunStreamResponseFixture.txt ([#674](https://github.com/openai-php/client/pull/674)) + # v0.16.0 (2025-08-26) ### Added