From e6392566f78eda7bf4b5d499cb5fe8323b29d65b Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Sat, 30 May 2026 07:32:27 -0400 Subject: [PATCH 1/8] feat(OpenAI): Add 'tool_search' functionality --- src/Actions/Responses/OutputObjects.php | 12 +++- src/Actions/Responses/ToolObjects.php | 12 +++- src/Responses/Responses/CreateResponse.php | 15 ++-- .../Responses/Output/OutputToolSearchCall.php | 71 +++++++++++++++++++ .../Output/OutputToolSearchOutput.php | 71 +++++++++++++++++++ src/Responses/Responses/RetrieveResponse.php | 15 ++-- .../Responses/Tool/NamespaceTool.php | 60 ++++++++++++++++ .../Responses/Tool/ToolSearchTool.php | 61 ++++++++++++++++ 8 files changed, 303 insertions(+), 14 deletions(-) create mode 100644 src/Responses/Responses/Output/OutputToolSearchCall.php create mode 100644 src/Responses/Responses/Output/OutputToolSearchOutput.php create mode 100644 src/Responses/Responses/Tool/NamespaceTool.php create mode 100644 src/Responses/Responses/Tool/ToolSearchTool.php diff --git a/src/Actions/Responses/OutputObjects.php b/src/Actions/Responses/OutputObjects.php index c5c90d47a..28672f4e5 100644 --- a/src/Actions/Responses/OutputObjects.php +++ b/src/Actions/Responses/OutputObjects.php @@ -16,6 +16,8 @@ use OpenAI\Responses\Responses\Output\OutputMcpListTools; use OpenAI\Responses\Responses\Output\OutputMessage; use OpenAI\Responses\Responses\Output\OutputReasoning; +use OpenAI\Responses\Responses\Output\OutputToolSearchCall; +use OpenAI\Responses\Responses\Output\OutputToolSearchOutput; use OpenAI\Responses\Responses\Output\OutputWebSearchToolCall; /** @@ -32,9 +34,11 @@ * @phpstan-import-type OutputCodeInterpreterToolCallType from OutputCodeInterpreterToolCall * @phpstan-import-type OutputLocalShellCallType from OutputLocalShellCall * @phpstan-import-type OutputCustomToolCallType from OutputCustomToolCall + * @phpstan-import-type OutputToolSearchCallType from OutputToolSearchCall + * @phpstan-import-type OutputToolSearchOutputType from OutputToolSearchOutput * - * @phpstan-type ResponseOutputObjectTypes array - * @phpstan-type ResponseOutputObjectReturnType array + * @phpstan-type ResponseOutputObjectTypes array + * @phpstan-type ResponseOutputObjectReturnType array */ final class OutputObjects { @@ -45,7 +49,7 @@ final class OutputObjects public static function parse(array $outputItems): array { return array_map( - fn (array $item): OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall|OutputLocalShellCall|OutputCustomToolCall => match ($item['type']) { + fn (array $item): OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall|OutputLocalShellCall|OutputCustomToolCall|OutputToolSearchCall|OutputToolSearchOutput => match ($item['type']) { 'message' => OutputMessage::from($item), 'file_search_call' => OutputFileSearchToolCall::from($item), 'function_call' => OutputFunctionToolCall::from($item), @@ -59,6 +63,8 @@ public static function parse(array $outputItems): array 'code_interpreter_call' => OutputCodeInterpreterToolCall::from($item), 'local_shell_call' => OutputLocalShellCall::from($item), 'custom_tool_call' => OutputCustomToolCall::from($item), + 'tool_search_call' => OutputToolSearchCall::from($item), + 'tool_search_output' => OutputToolSearchOutput::from($item), }, $outputItems, ); diff --git a/src/Actions/Responses/ToolObjects.php b/src/Actions/Responses/ToolObjects.php index 774d04cc5..a1f1869d6 100644 --- a/src/Actions/Responses/ToolObjects.php +++ b/src/Actions/Responses/ToolObjects.php @@ -9,7 +9,9 @@ use OpenAI\Responses\Responses\Tool\FileSearchTool; use OpenAI\Responses\Responses\Tool\FunctionTool; use OpenAI\Responses\Responses\Tool\ImageGenerationTool; +use OpenAI\Responses\Responses\Tool\NamespaceTool; use OpenAI\Responses\Responses\Tool\RemoteMcpTool; +use OpenAI\Responses\Responses\Tool\ToolSearchTool; use OpenAI\Responses\Responses\Tool\WebSearchTool; /** @@ -20,9 +22,11 @@ * @phpstan-import-type FunctionToolType from FunctionTool * @phpstan-import-type WebSearchToolType from WebSearchTool * @phpstan-import-type CodeInterpreterToolType from CodeInterpreterTool + * @phpstan-import-type ToolSearchToolType from ToolSearchTool + * @phpstan-import-type NamespaceToolType from NamespaceTool * - * @phpstan-type ResponseToolObjectTypes array - * @phpstan-type ResponseToolObjectReturnType array + * @phpstan-type ResponseToolObjectTypes array + * @phpstan-type ResponseToolObjectReturnType array */ final class ToolObjects { @@ -33,7 +37,7 @@ final class ToolObjects public static function parse(array $toolItems): array { return array_map( - fn (array $tool): ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool => match ($tool['type']) { + fn (array $tool): ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool|ToolSearchTool|NamespaceTool => 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), @@ -41,6 +45,8 @@ public static function parse(array $toolItems): array 'image_generation' => ImageGenerationTool::from($tool), 'mcp' => RemoteMcpTool::from($tool), 'code_interpreter' => CodeInterpreterTool::from($tool), + 'tool_search' => ToolSearchTool::from($tool), + 'namespace' => NamespaceTool::from($tool), }, $toolItems, ); diff --git a/src/Responses/Responses/CreateResponse.php b/src/Responses/Responses/CreateResponse.php index 6d7db8465..63d5106d3 100644 --- a/src/Responses/Responses/CreateResponse.php +++ b/src/Responses/Responses/CreateResponse.php @@ -25,13 +25,17 @@ use OpenAI\Responses\Responses\Output\OutputMcpListTools; use OpenAI\Responses\Responses\Output\OutputMessage; use OpenAI\Responses\Responses\Output\OutputReasoning; +use OpenAI\Responses\Responses\Output\OutputToolSearchCall; +use OpenAI\Responses\Responses\Output\OutputToolSearchOutput; use OpenAI\Responses\Responses\Output\OutputWebSearchToolCall; use OpenAI\Responses\Responses\Tool\CodeInterpreterTool; 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\NamespaceTool; use OpenAI\Responses\Responses\Tool\RemoteMcpTool; +use OpenAI\Responses\Responses\Tool\ToolSearchTool; use OpenAI\Responses\Responses\Tool\WebSearchTool; use OpenAI\Responses\Responses\ToolChoice\FunctionToolChoice; use OpenAI\Responses\Responses\ToolChoice\HostedToolChoice; @@ -47,6 +51,9 @@ * @phpstan-import-type ResponseOutputObjectTypes from OutputObjects * @phpstan-import-type ResponseToolChoiceTypes from ToolChoiceObjects * @phpstan-import-type ResponseToolObjectTypes from ToolObjects + * @phpstan-import-type ResponseOutputObjectReturnType from OutputObjects + * @phpstan-import-type ResponseToolChoiceReturnType from ToolChoiceObjects + * @phpstan-import-type ResponseToolObjectReturnType from ToolObjects * * @phpstan-type InstructionsType array|string|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} @@ -67,8 +74,8 @@ final class CreateResponse implements ResponseContract, ResponseHasMetaInformati * @param 'response' $object * @param 'completed'|'failed'|'in_progress'|'incomplete' $status * @param array|string|null $instructions - * @param array $output - * @param array $tools + * @param ResponseOutputObjectReturnType $output + * @param ResponseToolObjectReturnType $tools * @param 'auto'|'disabled'|null $truncation * @param array $metadata */ @@ -187,7 +194,7 @@ public function toArray(): array 'metadata' => $this->metadata ?? [], 'model' => $this->model, 'output' => array_map( - fn (OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall|OutputLocalShellCall|OutputCustomToolCall $output): array => $output->toArray(), + fn (OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall|OutputLocalShellCall|OutputCustomToolCall|OutputToolSearchCall|OutputToolSearchOutput $output): array => $output->toArray(), $this->output ), 'parallel_tool_calls' => $this->parallelToolCalls, @@ -204,7 +211,7 @@ public function toArray(): array ? $this->toolChoice : $this->toolChoice->toArray(), 'tools' => array_map( - fn (ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool $tool): array => $tool->toArray(), + fn (ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool|ToolSearchTool|NamespaceTool $tool): array => $tool->toArray(), $this->tools ), 'top_logprobs' => $this->topLogProbs, diff --git a/src/Responses/Responses/Output/OutputToolSearchCall.php b/src/Responses/Responses/Output/OutputToolSearchCall.php new file mode 100644 index 000000000..48107f545 --- /dev/null +++ b/src/Responses/Responses/Output/OutputToolSearchCall.php @@ -0,0 +1,71 @@ + + */ +final class OutputToolSearchCall implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'server'|'client' $execution + * @param 'in_progress'|'completed'|'incomplete' $status + * @param 'tool_search_call' $type + */ + private function __construct( + public readonly string $id, + public readonly mixed $arguments, + public readonly ?string $callId, + public readonly string $execution, + public readonly string $status, + public readonly string $type, + public readonly ?string $createdBy, + ) {} + + /** + * @param OutputToolSearchCallType $attributes + */ + public static function from(array $attributes): self + { + return new self( + id: $attributes['id'], + arguments: $attributes['arguments'], + callId: $attributes['call_id'] ?? null, + execution: $attributes['execution'], + status: $attributes['status'], + type: $attributes['type'], + createdBy: $attributes['created_by'] ?? null, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'arguments' => $this->arguments, + 'call_id' => $this->callId, + 'execution' => $this->execution, + 'status' => $this->status, + 'type' => $this->type, + 'createdBy' => $this->createdBy, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputToolSearchOutput.php b/src/Responses/Responses/Output/OutputToolSearchOutput.php new file mode 100644 index 000000000..1718cf628 --- /dev/null +++ b/src/Responses/Responses/Output/OutputToolSearchOutput.php @@ -0,0 +1,71 @@ + + */ +final class OutputToolSearchOutput implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'server'|'client' $execution + * @param 'in_progress'|'completed'|'incomplete' $status + * @param 'tool_search_output' $type + */ + private function __construct( + public readonly string $id, + public readonly ?string $callId, + public readonly string $execution, + public readonly string $status, + public readonly mixed $tools, + public readonly string $type, + public readonly ?string $createdBy, + ) {} + + /** + * @param OutputToolSearchOutputType $attributes + */ + public static function from(array $attributes): self + { + return new self( + id: $attributes['id'], + callId: $attributes['call_id'] ?? null, + execution: $attributes['execution'], + status: $attributes['status'], + tools: $attributes['tools'], + type: $attributes['type'], + createdBy: $attributes['created_by'] ?? null, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'call_id' => $this->callId, + 'execution' => $this->execution, + 'status' => $this->status, + 'tools' => $this->tools, + 'type' => $this->type, + 'created_by' => $this->createdBy, + ]; + } +} diff --git a/src/Responses/Responses/RetrieveResponse.php b/src/Responses/Responses/RetrieveResponse.php index 097a1156e..505741eeb 100644 --- a/src/Responses/Responses/RetrieveResponse.php +++ b/src/Responses/Responses/RetrieveResponse.php @@ -25,13 +25,17 @@ use OpenAI\Responses\Responses\Output\OutputMcpListTools; use OpenAI\Responses\Responses\Output\OutputMessage; use OpenAI\Responses\Responses\Output\OutputReasoning; +use OpenAI\Responses\Responses\Output\OutputToolSearchCall; +use OpenAI\Responses\Responses\Output\OutputToolSearchOutput; use OpenAI\Responses\Responses\Output\OutputWebSearchToolCall; use OpenAI\Responses\Responses\Tool\CodeInterpreterTool; 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\NamespaceTool; use OpenAI\Responses\Responses\Tool\RemoteMcpTool; +use OpenAI\Responses\Responses\Tool\ToolSearchTool; use OpenAI\Responses\Responses\Tool\WebSearchTool; use OpenAI\Responses\Responses\ToolChoice\FunctionToolChoice; use OpenAI\Responses\Responses\ToolChoice\HostedToolChoice; @@ -47,6 +51,9 @@ * @phpstan-import-type ResponseOutputObjectTypes from OutputObjects * @phpstan-import-type ResponseToolChoiceTypes from ToolChoiceObjects * @phpstan-import-type ResponseToolObjectTypes from ToolObjects + * @phpstan-import-type ResponseOutputObjectReturnType from OutputObjects + * @phpstan-import-type ResponseToolChoiceReturnType from ToolChoiceObjects + * @phpstan-import-type ResponseToolObjectReturnType from ToolObjects * * @phpstan-type InstructionsType array|string|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|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} @@ -67,8 +74,8 @@ final class RetrieveResponse implements ResponseContract, ResponseHasMetaInforma * @param 'response' $object * @param 'completed'|'failed'|'in_progress'|'incomplete' $status * @param array|string|null $instructions - * @param array $output - * @param array $tools + * @param ResponseOutputObjectReturnType $output + * @param ResponseToolObjectReturnType $tools * @param 'auto'|'disabled'|null $truncation * @param array $metadata */ @@ -187,7 +194,7 @@ public function toArray(): array 'metadata' => $this->metadata ?? [], 'model' => $this->model, 'output' => array_map( - fn (OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall|OutputLocalShellCall|OutputCustomToolCall $output): array => $output->toArray(), + fn (OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall|OutputLocalShellCall|OutputCustomToolCall|OutputToolSearchCall|OutputToolSearchOutput $output): array => $output->toArray(), $this->output ), 'output_text' => $this->outputText, @@ -205,7 +212,7 @@ public function toArray(): array ? $this->toolChoice : $this->toolChoice->toArray(), 'tools' => array_map( - fn (ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool $tool): array => $tool->toArray(), + fn (ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool|ToolSearchTool|NamespaceTool $tool): array => $tool->toArray(), $this->tools ), 'top_logprobs' => $this->topLogProbs, diff --git a/src/Responses/Responses/Tool/NamespaceTool.php b/src/Responses/Responses/Tool/NamespaceTool.php new file mode 100644 index 000000000..3de2096a8 --- /dev/null +++ b/src/Responses/Responses/Tool/NamespaceTool.php @@ -0,0 +1,60 @@ + + */ +final class NamespaceTool implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'namespace' $type + */ + private function __construct( + public readonly string $description, + public readonly string $name, + public readonly mixed $tools, + public readonly string $type, + ) {} + + /** + * @param NamespaceToolType $attributes + */ + public static function from(array $attributes): self + { + return new self( + description: $attributes['description'], + name: $attributes['name'], + tools: $attributes['tools'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'description' => $this->description, + 'name' => $this->name, + 'tools' => $this->tools, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Tool/ToolSearchTool.php b/src/Responses/Responses/Tool/ToolSearchTool.php new file mode 100644 index 000000000..9c69ad14f --- /dev/null +++ b/src/Responses/Responses/Tool/ToolSearchTool.php @@ -0,0 +1,61 @@ + + */ +final class ToolSearchTool implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'tool_search' $type + * @param 'server'|'client'|null $execution + */ + private function __construct( + public readonly string $type, + public readonly ?string $description, + public readonly ?string $execution, + public readonly mixed $parameters + ) {} + + /** + * @param ToolSearchToolType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + description: $attributes['description'] ?? null, + execution: $attributes['execution'] ?? null, + parameters: $attributes['parameters'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'description' => $this->description, + 'execution' => $this->execution, + 'parameters' => $this->parameters, + ]; + } +} From 7f87a7414b0ac0e5faa22985634c4f346d91c5ae Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Sat, 30 May 2026 07:38:15 -0400 Subject: [PATCH 2/8] test(OpenAI): Add 'tool_search' functionality --- .../Responses/Output/OutputToolSearchCall.php | 2 +- tests/Fixtures/Responses.php | 58 +++++++++++++++++++ .../Responses/Output/OutputToolSearchCall.php | 31 ++++++++++ .../Output/OutputToolSearchOutput.php | 31 ++++++++++ .../Responses/Tool/NamespaceTool.php | 28 +++++++++ .../Responses/Tool/ToolSearchTool.php | 28 +++++++++ 6 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 tests/Responses/Responses/Output/OutputToolSearchCall.php create mode 100644 tests/Responses/Responses/Output/OutputToolSearchOutput.php create mode 100644 tests/Responses/Responses/Tool/NamespaceTool.php create mode 100644 tests/Responses/Responses/Tool/ToolSearchTool.php diff --git a/src/Responses/Responses/Output/OutputToolSearchCall.php b/src/Responses/Responses/Output/OutputToolSearchCall.php index 48107f545..2a944cd75 100644 --- a/src/Responses/Responses/Output/OutputToolSearchCall.php +++ b/src/Responses/Responses/Output/OutputToolSearchCall.php @@ -65,7 +65,7 @@ public function toArray(): array 'execution' => $this->execution, 'status' => $this->status, 'type' => $this->type, - 'createdBy' => $this->createdBy, + 'created_by' => $this->createdBy, ]; } } diff --git a/tests/Fixtures/Responses.php b/tests/Fixtures/Responses.php index b0eda37ab..ad445ab97 100644 --- a/tests/Fixtures/Responses.php +++ b/tests/Fixtures/Responses.php @@ -903,6 +903,64 @@ function toolFileSearchNestedFilters(): array ]; } +/** + * @return array + */ +function toolNamespace(): array +{ + return [ + 'description' => 'A namespace of tools.', + 'name' => 'my_namespace', + 'tools' => [], + 'type' => 'namespace', + ]; +} + +/** + * @return array + */ +function toolToolSearch(): array +{ + return [ + 'type' => 'tool_search', + 'description' => 'A tool for searching tools.', + 'execution' => 'server', + 'parameters' => [], + ]; +} + +/** + * @return array + */ +function outputToolSearchCall(): array +{ + return [ + 'id' => 'tsc_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'arguments' => [], + 'call_id' => 'call_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'execution' => 'server', + 'status' => 'completed', + 'type' => 'tool_search_call', + 'created_by' => 'user_123', + ]; +} + +/** + * @return array + */ +function outputToolSearchOutput(): array +{ + return [ + 'id' => 'tso_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'call_id' => 'call_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'execution' => 'server', + 'status' => 'completed', + 'tools' => [], + 'type' => 'tool_search_output', + 'created_by' => 'user_123', + ]; +} + /** * @return resource */ diff --git a/tests/Responses/Responses/Output/OutputToolSearchCall.php b/tests/Responses/Responses/Output/OutputToolSearchCall.php new file mode 100644 index 000000000..6410a9ade --- /dev/null +++ b/tests/Responses/Responses/Output/OutputToolSearchCall.php @@ -0,0 +1,31 @@ +toBeInstanceOf(OutputToolSearchCall::class) + ->id->toBe('tsc_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c') + ->arguments->toBeArray() + ->callId->toBe('call_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c') + ->execution->toBe('server') + ->status->toBe('completed') + ->type->toBe('tool_search_call') + ->createdBy->toBe('user_123'); +}); + +test('as array accessible', function () { + $response = OutputToolSearchCall::from(outputToolSearchCall()); + + expect($response['id'])->toBe('tsc_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c'); +}); + +test('to array', function () { + $response = OutputToolSearchCall::from(outputToolSearchCall()); + + expect($response->toArray()) + ->toBeArray() + ->toBe(outputToolSearchCall()); +}); diff --git a/tests/Responses/Responses/Output/OutputToolSearchOutput.php b/tests/Responses/Responses/Output/OutputToolSearchOutput.php new file mode 100644 index 000000000..c939d1726 --- /dev/null +++ b/tests/Responses/Responses/Output/OutputToolSearchOutput.php @@ -0,0 +1,31 @@ +toBeInstanceOf(OutputToolSearchOutput::class) + ->id->toBe('tso_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c') + ->callId->toBe('call_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c') + ->execution->toBe('server') + ->status->toBe('completed') + ->tools->toBeArray() + ->type->toBe('tool_search_output') + ->createdBy->toBe('user_123'); +}); + +test('as array accessible', function () { + $response = OutputToolSearchOutput::from(outputToolSearchOutput()); + + expect($response['id'])->toBe('tso_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c'); +}); + +test('to array', function () { + $response = OutputToolSearchOutput::from(outputToolSearchOutput()); + + expect($response->toArray()) + ->toBeArray() + ->toBe(outputToolSearchOutput()); +}); diff --git a/tests/Responses/Responses/Tool/NamespaceTool.php b/tests/Responses/Responses/Tool/NamespaceTool.php new file mode 100644 index 000000000..58019c381 --- /dev/null +++ b/tests/Responses/Responses/Tool/NamespaceTool.php @@ -0,0 +1,28 @@ +toBeInstanceOf(NamespaceTool::class) + ->description->toBe('A namespace of tools.') + ->name->toBe('my_namespace') + ->tools->toBeArray() + ->type->toBe('namespace'); +}); + +test('as array accessible', function () { + $response = NamespaceTool::from(toolNamespace()); + + expect($response['type'])->toBe('namespace'); +}); + +test('to array', function () { + $response = NamespaceTool::from(toolNamespace()); + + expect($response->toArray()) + ->toBeArray() + ->toBe(toolNamespace()); +}); diff --git a/tests/Responses/Responses/Tool/ToolSearchTool.php b/tests/Responses/Responses/Tool/ToolSearchTool.php new file mode 100644 index 000000000..81785358c --- /dev/null +++ b/tests/Responses/Responses/Tool/ToolSearchTool.php @@ -0,0 +1,28 @@ +toBeInstanceOf(ToolSearchTool::class) + ->type->toBe('tool_search') + ->description->toBe('A tool for searching tools.') + ->execution->toBe('server') + ->parameters->toBeArray(); +}); + +test('as array accessible', function () { + $response = ToolSearchTool::from(toolToolSearch()); + + expect($response['type'])->toBe('tool_search'); +}); + +test('to array', function () { + $response = ToolSearchTool::from(toolToolSearch()); + + expect($response->toArray()) + ->toBeArray() + ->toBe(toolToolSearch()); +}); From e631e3b89d57062b5f6a66164cf1713db26ea87a Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Sat, 30 May 2026 08:05:25 -0400 Subject: [PATCH 3/8] fix(OpenAI): chore add custom_tool --- .../Responses/CustomToolInputObjects.php | 29 ++++++++ src/Actions/Responses/ToolObjects.php | 9 ++- src/Responses/Responses/CreateResponse.php | 3 +- src/Responses/Responses/RetrieveResponse.php | 3 +- src/Responses/Responses/Tool/CustomTool.php | 70 +++++++++++++++++++ .../Tool/CustomToolInputs/GrammarInput.php | 57 +++++++++++++++ .../Tool/CustomToolInputs/TextInput.php | 51 ++++++++++++++ .../Responses/Tool/NamespaceTool.php | 22 ++++-- tests/Fixtures/Responses.php | 16 +++++ .../Responses/Tool/CustomToolTest.php | 43 ++++++++++++ 10 files changed, 294 insertions(+), 9 deletions(-) create mode 100644 src/Actions/Responses/CustomToolInputObjects.php create mode 100644 src/Responses/Responses/Tool/CustomTool.php create mode 100644 src/Responses/Responses/Tool/CustomToolInputs/GrammarInput.php create mode 100644 src/Responses/Responses/Tool/CustomToolInputs/TextInput.php create mode 100644 tests/Responses/Responses/Tool/CustomToolTest.php diff --git a/src/Actions/Responses/CustomToolInputObjects.php b/src/Actions/Responses/CustomToolInputObjects.php new file mode 100644 index 000000000..ede190bfd --- /dev/null +++ b/src/Actions/Responses/CustomToolInputObjects.php @@ -0,0 +1,29 @@ + TextInput::from($attributes), + 'grammar' => GrammarInput::from($attributes), + }; + } +} diff --git a/src/Actions/Responses/ToolObjects.php b/src/Actions/Responses/ToolObjects.php index a1f1869d6..5726b1fb5 100644 --- a/src/Actions/Responses/ToolObjects.php +++ b/src/Actions/Responses/ToolObjects.php @@ -6,6 +6,7 @@ use OpenAI\Responses\Responses\Tool\CodeInterpreterTool; use OpenAI\Responses\Responses\Tool\ComputerUseTool; +use OpenAI\Responses\Responses\Tool\CustomTool; use OpenAI\Responses\Responses\Tool\FileSearchTool; use OpenAI\Responses\Responses\Tool\FunctionTool; use OpenAI\Responses\Responses\Tool\ImageGenerationTool; @@ -24,9 +25,10 @@ * @phpstan-import-type CodeInterpreterToolType from CodeInterpreterTool * @phpstan-import-type ToolSearchToolType from ToolSearchTool * @phpstan-import-type NamespaceToolType from NamespaceTool + * @phpstan-import-type CustomToolType from CustomTool * - * @phpstan-type ResponseToolObjectTypes array - * @phpstan-type ResponseToolObjectReturnType array + * @phpstan-type ResponseToolObjectTypes array + * @phpstan-type ResponseToolObjectReturnType array */ final class ToolObjects { @@ -37,7 +39,7 @@ final class ToolObjects public static function parse(array $toolItems): array { return array_map( - fn (array $tool): ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool|ToolSearchTool|NamespaceTool => match ($tool['type']) { + fn (array $tool): ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool|ToolSearchTool|NamespaceTool|CustomTool => 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), @@ -47,6 +49,7 @@ public static function parse(array $toolItems): array 'code_interpreter' => CodeInterpreterTool::from($tool), 'tool_search' => ToolSearchTool::from($tool), 'namespace' => NamespaceTool::from($tool), + 'custom' => CustomTool::from($tool), }, $toolItems, ); diff --git a/src/Responses/Responses/CreateResponse.php b/src/Responses/Responses/CreateResponse.php index 63d5106d3..6c4a78526 100644 --- a/src/Responses/Responses/CreateResponse.php +++ b/src/Responses/Responses/CreateResponse.php @@ -30,6 +30,7 @@ use OpenAI\Responses\Responses\Output\OutputWebSearchToolCall; use OpenAI\Responses\Responses\Tool\CodeInterpreterTool; use OpenAI\Responses\Responses\Tool\ComputerUseTool; +use OpenAI\Responses\Responses\Tool\CustomTool; use OpenAI\Responses\Responses\Tool\FileSearchTool; use OpenAI\Responses\Responses\Tool\FunctionTool; use OpenAI\Responses\Responses\Tool\ImageGenerationTool; @@ -211,7 +212,7 @@ public function toArray(): array ? $this->toolChoice : $this->toolChoice->toArray(), 'tools' => array_map( - fn (ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool|ToolSearchTool|NamespaceTool $tool): array => $tool->toArray(), + fn (ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool|ToolSearchTool|NamespaceTool|CustomTool $tool): array => $tool->toArray(), $this->tools ), 'top_logprobs' => $this->topLogProbs, diff --git a/src/Responses/Responses/RetrieveResponse.php b/src/Responses/Responses/RetrieveResponse.php index 505741eeb..2c1d99099 100644 --- a/src/Responses/Responses/RetrieveResponse.php +++ b/src/Responses/Responses/RetrieveResponse.php @@ -30,6 +30,7 @@ use OpenAI\Responses\Responses\Output\OutputWebSearchToolCall; use OpenAI\Responses\Responses\Tool\CodeInterpreterTool; use OpenAI\Responses\Responses\Tool\ComputerUseTool; +use OpenAI\Responses\Responses\Tool\CustomTool; use OpenAI\Responses\Responses\Tool\FileSearchTool; use OpenAI\Responses\Responses\Tool\FunctionTool; use OpenAI\Responses\Responses\Tool\ImageGenerationTool; @@ -212,7 +213,7 @@ public function toArray(): array ? $this->toolChoice : $this->toolChoice->toArray(), 'tools' => array_map( - fn (ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool|ToolSearchTool|NamespaceTool $tool): array => $tool->toArray(), + fn (ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool|ToolSearchTool|NamespaceTool|CustomTool $tool): array => $tool->toArray(), $this->tools ), 'top_logprobs' => $this->topLogProbs, diff --git a/src/Responses/Responses/Tool/CustomTool.php b/src/Responses/Responses/Tool/CustomTool.php new file mode 100644 index 000000000..d670cc2b6 --- /dev/null +++ b/src/Responses/Responses/Tool/CustomTool.php @@ -0,0 +1,70 @@ + + */ +final class CustomTool implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'custom' $type + */ + private function __construct( + public readonly string $name, + public readonly string $type, + public readonly ?bool $deferLoading = null, + public readonly ?string $description = null, + public readonly TextInput|GrammarInput|null $format = null, + ) {} + + /** + * @param CustomToolType $attributes + */ + public static function from(array $attributes): self + { + return new self( + name: $attributes['name'], + type: $attributes['type'], + deferLoading: $attributes['defer_loading'] ?? null, + description: $attributes['description'] ?? null, + format: isset($attributes['format']) + ? CustomToolInputObjects::parse($attributes['format']) + : null, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return array_filter([ + 'name' => $this->name, + 'type' => $this->type, + 'defer_loading' => $this->deferLoading, + 'description' => $this->description, + 'format' => $this->format?->toArray(), + ], fn (mixed $value): bool => $value !== null); + } +} diff --git a/src/Responses/Responses/Tool/CustomToolInputs/GrammarInput.php b/src/Responses/Responses/Tool/CustomToolInputs/GrammarInput.php new file mode 100644 index 000000000..31d9ceb96 --- /dev/null +++ b/src/Responses/Responses/Tool/CustomToolInputs/GrammarInput.php @@ -0,0 +1,57 @@ + + */ +final class GrammarInput implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'grammar' $type + */ + private function __construct( + public readonly string $type, + public readonly string $definition, + public readonly string $syntax, + ) {} + + /** + * @param GrammarInputType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + definition: $attributes['definition'], + syntax: $attributes['syntax'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'definition' => $this->definition, + 'syntax' => $this->syntax, + ]; + } +} diff --git a/src/Responses/Responses/Tool/CustomToolInputs/TextInput.php b/src/Responses/Responses/Tool/CustomToolInputs/TextInput.php new file mode 100644 index 000000000..215545ce7 --- /dev/null +++ b/src/Responses/Responses/Tool/CustomToolInputs/TextInput.php @@ -0,0 +1,51 @@ + + */ +final class TextInput implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'text' $type + */ + private function __construct( + public readonly string $type, + ) {} + + /** + * @param TextInputType $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/Tool/NamespaceTool.php b/src/Responses/Responses/Tool/NamespaceTool.php index 3de2096a8..1b1a57f74 100644 --- a/src/Responses/Responses/Tool/NamespaceTool.php +++ b/src/Responses/Responses/Tool/NamespaceTool.php @@ -4,12 +4,19 @@ namespace OpenAI\Responses\Responses\Tool; +use OpenAI\Actions\Responses\ToolObjects; use OpenAI\Contracts\ResponseContract; use OpenAI\Responses\Concerns\ArrayAccessible; +use OpenAI\Responses\Responses\Tool\CustomTool; +use OpenAI\Responses\Responses\Tool\FunctionTool; use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @phpstan-type NamespaceToolType array{description: string, name: string, tools: mixed, type: 'namespace'} + * @phpstan-import-type FunctionToolType from FunctionTool + * @phpstan-import-type CustomToolType from CustomTool + * @phpstan-import-type ResponseToolObjectReturnType from ToolObjects + * + * @phpstan-type NamespaceToolType array{description: string, name: string, tools: array, type: 'namespace'} * * @implements ResponseContract */ @@ -24,11 +31,12 @@ final class NamespaceTool implements ResponseContract /** * @param 'namespace' $type + * @param array $tools */ private function __construct( public readonly string $description, public readonly string $name, - public readonly mixed $tools, + public readonly array $tools, public readonly string $type, ) {} @@ -37,10 +45,13 @@ private function __construct( */ public static function from(array $attributes): self { + /** @var array $tools */ + $tools = ToolObjects::parse($attributes['tools']); + return new self( description: $attributes['description'], name: $attributes['name'], - tools: $attributes['tools'], + tools: $tools, type: $attributes['type'], ); } @@ -53,7 +64,10 @@ public function toArray(): array return [ 'description' => $this->description, 'name' => $this->name, - 'tools' => $this->tools, + 'tools' => array_map( + fn (FunctionTool|CustomTool $tool): array => $tool->toArray(), + $this->tools, + ), 'type' => $this->type, ]; } diff --git a/tests/Fixtures/Responses.php b/tests/Fixtures/Responses.php index ad445ab97..6c8556892 100644 --- a/tests/Fixtures/Responses.php +++ b/tests/Fixtures/Responses.php @@ -903,6 +903,22 @@ function toolFileSearchNestedFilters(): array ]; } +/** + * @return array + */ +function toolCustom(): array +{ + return [ + 'name' => 'my_custom_tool', + 'type' => 'custom', + 'defer_loading' => false, + 'description' => 'A custom tool.', + 'format' => [ + 'type' => 'text', + ], + ]; +} + /** * @return array */ diff --git a/tests/Responses/Responses/Tool/CustomToolTest.php b/tests/Responses/Responses/Tool/CustomToolTest.php new file mode 100644 index 000000000..93b376b66 --- /dev/null +++ b/tests/Responses/Responses/Tool/CustomToolTest.php @@ -0,0 +1,43 @@ +toBeInstanceOf(CustomTool::class) + ->name->toBe('my_custom_tool') + ->type->toBe('custom') + ->deferLoading->toBeFalse() + ->description->toBe('A custom tool.') + ->format->toBeInstanceOf(TextInput::class) + ->format->type->toBe('text'); +}); + +test('to array with custom tool', function () { + $response = CustomTool::from(toolCustom()); + + expect($response->toArray()) + ->toBeArray() + ->toBe(toolCustom()); +}); + +test('namespace tool with custom tool', function () { + $data = toolNamespace(); + $data['tools'] = [ + toolCustom(), + ]; + + $response = NamespaceTool::from($data); + + expect($response) + ->toBeInstanceOf(NamespaceTool::class) + ->tools->toHaveCount(1) + ->tools->{0}->toBeInstanceOf(CustomTool::class); + + expect($response->toArray()) + ->toBe($data); +}); From d4a7d143354092409502a81d8184c05e50014029 Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Sat, 30 May 2026 08:13:41 -0400 Subject: [PATCH 4/8] fix(OpenAI): chore add namespace --- .../Responses/NamespaceToolObjects.php | 35 ++++++++++ src/Actions/Responses/ToolObjects.php | 36 ++++------ .../Responses/Tool/NamespaceTool.php | 10 +-- .../Tool/NamespaceTools/CustomTool.php | 70 +++++++++++++++++++ .../Tool/NamespaceTools/FunctionTool.php | 64 +++++++++++++++++ .../Responses/Tool/CustomToolTest.php | 3 +- 6 files changed, 189 insertions(+), 29 deletions(-) create mode 100644 src/Actions/Responses/NamespaceToolObjects.php create mode 100644 src/Responses/Responses/Tool/NamespaceTools/CustomTool.php create mode 100644 src/Responses/Responses/Tool/NamespaceTools/FunctionTool.php diff --git a/src/Actions/Responses/NamespaceToolObjects.php b/src/Actions/Responses/NamespaceToolObjects.php new file mode 100644 index 000000000..94a36faff --- /dev/null +++ b/src/Actions/Responses/NamespaceToolObjects.php @@ -0,0 +1,35 @@ + + * @phpstan-type ResponseNamespaceToolObjectReturnType array + */ +final class NamespaceToolObjects +{ + /** + * @param ResponseNamespaceToolObjectTypes $toolItems + * @return ResponseNamespaceToolObjectReturnType + */ + public static function parse(array $toolItems): array + { + return array_map( + fn (array $tool): FunctionTool|CustomTool => match ($tool['type']) { + 'function' => FunctionTool::from($tool), + 'custom' => CustomTool::from($tool), + /** @phpstan-ignore-next-line */ + default => throw new \InvalidArgumentException("Unknown tool type: {$tool['type']}"), + }, + $toolItems, + ); + } +} diff --git a/src/Actions/Responses/ToolObjects.php b/src/Actions/Responses/ToolObjects.php index 5726b1fb5..af1d07d06 100644 --- a/src/Actions/Responses/ToolObjects.php +++ b/src/Actions/Responses/ToolObjects.php @@ -16,19 +16,8 @@ use OpenAI\Responses\Responses\Tool\WebSearchTool; /** - * @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 ToolSearchToolType from ToolSearchTool - * @phpstan-import-type NamespaceToolType from NamespaceTool - * @phpstan-import-type CustomToolType from CustomTool - * - * @phpstan-type ResponseToolObjectTypes array - * @phpstan-type ResponseToolObjectReturnType array + * @phpstan-type ResponseToolObjectTypes array + * @phpstan-type ResponseToolObjectReturnType array */ final class ToolObjects { @@ -40,16 +29,17 @@ public static function parse(array $toolItems): array { return array_map( fn (array $tool): ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool|ToolSearchTool|NamespaceTool|CustomTool => 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), - 'tool_search' => ToolSearchTool::from($tool), - 'namespace' => NamespaceTool::from($tool), - 'custom' => CustomTool::from($tool), + 'file_search' => FileSearchTool::from($tool), // @phpstan-ignore-line + 'web_search', 'web_search_preview', 'web_search_preview_2025_03_11' => WebSearchTool::from($tool), // @phpstan-ignore-line + 'function' => FunctionTool::from($tool), // @phpstan-ignore-line + 'computer_use_preview' => ComputerUseTool::from($tool), // @phpstan-ignore-line + 'image_generation' => ImageGenerationTool::from($tool), // @phpstan-ignore-line + 'mcp' => RemoteMcpTool::from($tool), // @phpstan-ignore-line + 'code_interpreter' => CodeInterpreterTool::from($tool), // @phpstan-ignore-line + 'tool_search' => ToolSearchTool::from($tool), // @phpstan-ignore-line + 'namespace' => NamespaceTool::from($tool), // @phpstan-ignore-line + 'custom' => CustomTool::from($tool), // @phpstan-ignore-line + default => throw new \InvalidArgumentException("Unknown tool type: {$tool['type']}"), }, $toolItems, ); diff --git a/src/Responses/Responses/Tool/NamespaceTool.php b/src/Responses/Responses/Tool/NamespaceTool.php index 1b1a57f74..4e5c20dc7 100644 --- a/src/Responses/Responses/Tool/NamespaceTool.php +++ b/src/Responses/Responses/Tool/NamespaceTool.php @@ -4,17 +4,17 @@ namespace OpenAI\Responses\Responses\Tool; -use OpenAI\Actions\Responses\ToolObjects; +use OpenAI\Actions\Responses\NamespaceToolObjects; use OpenAI\Contracts\ResponseContract; use OpenAI\Responses\Concerns\ArrayAccessible; -use OpenAI\Responses\Responses\Tool\CustomTool; -use OpenAI\Responses\Responses\Tool\FunctionTool; +use OpenAI\Responses\Responses\Tool\NamespaceTools\CustomTool; +use OpenAI\Responses\Responses\Tool\NamespaceTools\FunctionTool; use OpenAI\Testing\Responses\Concerns\Fakeable; /** * @phpstan-import-type FunctionToolType from FunctionTool * @phpstan-import-type CustomToolType from CustomTool - * @phpstan-import-type ResponseToolObjectReturnType from ToolObjects + * @phpstan-import-type ResponseNamespaceToolObjectReturnType from NamespaceToolObjects * * @phpstan-type NamespaceToolType array{description: string, name: string, tools: array, type: 'namespace'} * @@ -46,7 +46,7 @@ private function __construct( public static function from(array $attributes): self { /** @var array $tools */ - $tools = ToolObjects::parse($attributes['tools']); + $tools = NamespaceToolObjects::parse($attributes['tools']); return new self( description: $attributes['description'], diff --git a/src/Responses/Responses/Tool/NamespaceTools/CustomTool.php b/src/Responses/Responses/Tool/NamespaceTools/CustomTool.php new file mode 100644 index 000000000..824fc22b2 --- /dev/null +++ b/src/Responses/Responses/Tool/NamespaceTools/CustomTool.php @@ -0,0 +1,70 @@ + + */ +final class CustomTool implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'custom' $type + */ + private function __construct( + public readonly string $name, + public readonly string $type, + public readonly ?bool $deferLoading = null, + public readonly ?string $description = null, + public readonly TextInput|GrammarInput|null $format = null, + ) {} + + /** + * @param CustomToolType $attributes + */ + public static function from(array $attributes): self + { + return new self( + name: $attributes['name'], + type: $attributes['type'], + deferLoading: $attributes['defer_loading'] ?? null, + description: $attributes['description'] ?? null, + format: isset($attributes['format']) + ? CustomToolInputObjects::parse($attributes['format']) + : null, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return array_filter([ + 'name' => $this->name, + 'type' => $this->type, + 'defer_loading' => $this->deferLoading, + 'description' => $this->description, + 'format' => $this->format?->toArray(), + ], fn (mixed $value): bool => $value !== null); + } +} diff --git a/src/Responses/Responses/Tool/NamespaceTools/FunctionTool.php b/src/Responses/Responses/Tool/NamespaceTools/FunctionTool.php new file mode 100644 index 000000000..623e8fe38 --- /dev/null +++ b/src/Responses/Responses/Tool/NamespaceTools/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/tests/Responses/Responses/Tool/CustomToolTest.php b/tests/Responses/Responses/Tool/CustomToolTest.php index 93b376b66..801d25a39 100644 --- a/tests/Responses/Responses/Tool/CustomToolTest.php +++ b/tests/Responses/Responses/Tool/CustomToolTest.php @@ -3,6 +3,7 @@ use OpenAI\Responses\Responses\Tool\CustomTool; use OpenAI\Responses\Responses\Tool\CustomToolInputs\TextInput; use OpenAI\Responses\Responses\Tool\NamespaceTool; +use OpenAI\Responses\Responses\Tool\NamespaceTools\CustomTool as NamespaceCustomTool; test('from with custom tool', function () { $response = CustomTool::from(toolCustom()); @@ -36,7 +37,7 @@ expect($response) ->toBeInstanceOf(NamespaceTool::class) ->tools->toHaveCount(1) - ->tools->{0}->toBeInstanceOf(CustomTool::class); + ->tools->{0}->toBeInstanceOf(NamespaceCustomTool::class); expect($response->toArray()) ->toBe($data); From 401154885d0ca0e5eaceafdbdd7f1c4e5a31a3fc Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Sat, 30 May 2026 08:18:16 -0400 Subject: [PATCH 5/8] chore(OpenAI): fix issue with phpstan ignores --- .../Responses/NamespaceToolObjects.php | 2 - src/Actions/Responses/ToolObjects.php | 38 ++++++++++++------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/Actions/Responses/NamespaceToolObjects.php b/src/Actions/Responses/NamespaceToolObjects.php index 94a36faff..b884a284a 100644 --- a/src/Actions/Responses/NamespaceToolObjects.php +++ b/src/Actions/Responses/NamespaceToolObjects.php @@ -26,8 +26,6 @@ public static function parse(array $toolItems): array fn (array $tool): FunctionTool|CustomTool => match ($tool['type']) { 'function' => FunctionTool::from($tool), 'custom' => CustomTool::from($tool), - /** @phpstan-ignore-next-line */ - default => throw new \InvalidArgumentException("Unknown tool type: {$tool['type']}"), }, $toolItems, ); diff --git a/src/Actions/Responses/ToolObjects.php b/src/Actions/Responses/ToolObjects.php index af1d07d06..a99c8a905 100644 --- a/src/Actions/Responses/ToolObjects.php +++ b/src/Actions/Responses/ToolObjects.php @@ -16,8 +16,19 @@ use OpenAI\Responses\Responses\Tool\WebSearchTool; /** - * @phpstan-type ResponseToolObjectTypes array - * @phpstan-type ResponseToolObjectReturnType array + * @phpstan-import-type CodeInterpreterToolType from CodeInterpreterTool + * @phpstan-import-type ComputerUseToolType from ComputerUseTool + * @phpstan-import-type CustomToolType from CustomTool + * @phpstan-import-type FileSearchToolType from FileSearchTool + * @phpstan-import-type FunctionToolType from FunctionTool + * @phpstan-import-type ImageGenerationToolType from ImageGenerationTool + * @phpstan-import-type NamespaceToolType from NamespaceTool + * @phpstan-import-type RemoteMcpToolType from RemoteMcpTool + * @phpstan-import-type ToolSearchToolType from ToolSearchTool + * @phpstan-import-type WebSearchToolType from WebSearchTool + * + * @phpstan-type ResponseToolObjectTypes array + * @phpstan-type ResponseToolObjectReturnType array */ final class ToolObjects { @@ -28,18 +39,17 @@ final class ToolObjects public static function parse(array $toolItems): array { return array_map( - fn (array $tool): ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool|ToolSearchTool|NamespaceTool|CustomTool => match ($tool['type']) { - 'file_search' => FileSearchTool::from($tool), // @phpstan-ignore-line - 'web_search', 'web_search_preview', 'web_search_preview_2025_03_11' => WebSearchTool::from($tool), // @phpstan-ignore-line - 'function' => FunctionTool::from($tool), // @phpstan-ignore-line - 'computer_use_preview' => ComputerUseTool::from($tool), // @phpstan-ignore-line - 'image_generation' => ImageGenerationTool::from($tool), // @phpstan-ignore-line - 'mcp' => RemoteMcpTool::from($tool), // @phpstan-ignore-line - 'code_interpreter' => CodeInterpreterTool::from($tool), // @phpstan-ignore-line - 'tool_search' => ToolSearchTool::from($tool), // @phpstan-ignore-line - 'namespace' => NamespaceTool::from($tool), // @phpstan-ignore-line - 'custom' => CustomTool::from($tool), // @phpstan-ignore-line - default => throw new \InvalidArgumentException("Unknown tool type: {$tool['type']}"), + fn (array $tool): CodeInterpreterTool|ComputerUseTool|CustomTool|FileSearchTool|FunctionTool|ImageGenerationTool|NamespaceTool|RemoteMcpTool|ToolSearchTool|WebSearchTool => 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), + 'tool_search' => ToolSearchTool::from($tool), + 'namespace' => NamespaceTool::from($tool), + 'custom' => CustomTool::from($tool), }, $toolItems, ); From 4017d83910096960585c583bd8839ee0f060ce96 Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Sat, 30 May 2026 08:44:34 -0400 Subject: [PATCH 6/8] chore(OpenAI): add typing for tools --- .../Output/OutputToolSearchOutput.php | 26 ++++++++++++++++--- tests/Fixtures/Responses.php | 8 +++++- .../Output/OutputToolSearchOutput.php | 3 +++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/Responses/Responses/Output/OutputToolSearchOutput.php b/src/Responses/Responses/Output/OutputToolSearchOutput.php index 1718cf628..8308530ff 100644 --- a/src/Responses/Responses/Output/OutputToolSearchOutput.php +++ b/src/Responses/Responses/Output/OutputToolSearchOutput.php @@ -4,12 +4,26 @@ namespace OpenAI\Responses\Responses\Output; +use OpenAI\Actions\Responses\ToolObjects; use OpenAI\Contracts\ResponseContract; use OpenAI\Responses\Concerns\ArrayAccessible; +use OpenAI\Responses\Responses\Tool\CodeInterpreterTool; +use OpenAI\Responses\Responses\Tool\ComputerUseTool; +use OpenAI\Responses\Responses\Tool\CustomTool; +use OpenAI\Responses\Responses\Tool\FileSearchTool; +use OpenAI\Responses\Responses\Tool\FunctionTool; +use OpenAI\Responses\Responses\Tool\ImageGenerationTool; +use OpenAI\Responses\Responses\Tool\NamespaceTool; +use OpenAI\Responses\Responses\Tool\RemoteMcpTool; +use OpenAI\Responses\Responses\Tool\ToolSearchTool; +use OpenAI\Responses\Responses\Tool\WebSearchTool; use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @phpstan-type OutputToolSearchOutputType array{id: string, call_id: ?string, execution: 'server'|'client', status: 'in_progress'|'completed'|'incomplete', tools: mixed, type: 'tool_search_output', created_by?: ?string} + * @phpstan-import-type ResponseToolObjectTypes from ToolObjects + * @phpstan-import-type ResponseToolObjectReturnType from ToolObjects + * + * @phpstan-type OutputToolSearchOutputType array{id: string, call_id: ?string, execution: 'server'|'client', status: 'in_progress'|'completed'|'incomplete', tools: ResponseToolObjectTypes, type: 'tool_search_output', created_by?: ?string} * * @implements ResponseContract */ @@ -25,6 +39,7 @@ final class OutputToolSearchOutput implements ResponseContract /** * @param 'server'|'client' $execution * @param 'in_progress'|'completed'|'incomplete' $status + * @param ResponseToolObjectReturnType $tools * @param 'tool_search_output' $type */ private function __construct( @@ -32,7 +47,7 @@ private function __construct( public readonly ?string $callId, public readonly string $execution, public readonly string $status, - public readonly mixed $tools, + public readonly array $tools, public readonly string $type, public readonly ?string $createdBy, ) {} @@ -47,7 +62,7 @@ public static function from(array $attributes): self callId: $attributes['call_id'] ?? null, execution: $attributes['execution'], status: $attributes['status'], - tools: $attributes['tools'], + tools: ToolObjects::parse($attributes['tools']), type: $attributes['type'], createdBy: $attributes['created_by'] ?? null, ); @@ -63,7 +78,10 @@ public function toArray(): array 'call_id' => $this->callId, 'execution' => $this->execution, 'status' => $this->status, - 'tools' => $this->tools, + 'tools' => array_map( + fn (CodeInterpreterTool|ComputerUseTool|CustomTool|FileSearchTool|FunctionTool|ImageGenerationTool|NamespaceTool|RemoteMcpTool|ToolSearchTool|WebSearchTool $tool): array => $tool->toArray(), + $this->tools, + ), 'type' => $this->type, 'created_by' => $this->createdBy, ]; diff --git a/tests/Fixtures/Responses.php b/tests/Fixtures/Responses.php index 6c8556892..5dfb701eb 100644 --- a/tests/Fixtures/Responses.php +++ b/tests/Fixtures/Responses.php @@ -971,7 +971,13 @@ function outputToolSearchOutput(): array 'call_id' => 'call_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', 'execution' => 'server', 'status' => 'completed', - 'tools' => [], + 'tools' => [ + [ + 'type' => 'web_search', + 'search_context_size' => 'low', + 'user_location' => null, + ], + ], 'type' => 'tool_search_output', 'created_by' => 'user_123', ]; diff --git a/tests/Responses/Responses/Output/OutputToolSearchOutput.php b/tests/Responses/Responses/Output/OutputToolSearchOutput.php index c939d1726..131cd8113 100644 --- a/tests/Responses/Responses/Output/OutputToolSearchOutput.php +++ b/tests/Responses/Responses/Output/OutputToolSearchOutput.php @@ -1,6 +1,7 @@ execution->toBe('server') ->status->toBe('completed') ->tools->toBeArray() + ->tools->toHaveCount(1) + ->tools->{0}->toBeInstanceOf(WebSearchTool::class) ->type->toBe('tool_search_output') ->createdBy->toBe('user_123'); }); From 0a8234fabce7487e686d719fe56013dc6f0ecbfd Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Sat, 30 May 2026 09:09:17 -0400 Subject: [PATCH 7/8] chore(OpenAI): throw on unknown --- src/Actions/Responses/OutputObjects.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Actions/Responses/OutputObjects.php b/src/Actions/Responses/OutputObjects.php index 28672f4e5..faa4dfc1c 100644 --- a/src/Actions/Responses/OutputObjects.php +++ b/src/Actions/Responses/OutputObjects.php @@ -65,6 +65,7 @@ public static function parse(array $outputItems): array 'custom_tool_call' => OutputCustomToolCall::from($item), 'tool_search_call' => OutputToolSearchCall::from($item), 'tool_search_output' => OutputToolSearchOutput::from($item), + default => throw new \UnexpectedValueException('Unknown output item type.'), }, $outputItems, ); From cb1ef7af617c39f03943e8583d5da1346c0e63b3 Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Sat, 30 May 2026 12:36:13 -0400 Subject: [PATCH 8/8] chore(OpenAI): cleaner message --- src/Actions/Responses/OutputObjects.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Actions/Responses/OutputObjects.php b/src/Actions/Responses/OutputObjects.php index faa4dfc1c..fca94ba6e 100644 --- a/src/Actions/Responses/OutputObjects.php +++ b/src/Actions/Responses/OutputObjects.php @@ -65,7 +65,7 @@ public static function parse(array $outputItems): array 'custom_tool_call' => OutputCustomToolCall::from($item), 'tool_search_call' => OutputToolSearchCall::from($item), 'tool_search_output' => OutputToolSearchOutput::from($item), - default => throw new \UnexpectedValueException('Unknown output item type.'), + default => throw new \UnexpectedValueException('Uh oh! We do not recognize this type. Please submit a bug to openai-php/client on GitHub!'), }, $outputItems, );