From bbda5b0ccd941b7229263b5fb5d386d99116d43d Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Mon, 6 Jul 2026 14:57:36 +0400 Subject: [PATCH 1/3] Support streaming compaction output --- .../Responses/Streaming/OutputItem.php | 7 +++++-- tests/Fixtures/Responses.php | 5 +++++ .../ResponseOutputItemCompactionDone.txt | 1 + .../Responses/CreateStreamedResponse.php | 17 +++++++++++++++++ 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 tests/Fixtures/Streams/ResponseOutputItemCompactionDone.txt diff --git a/src/Responses/Responses/Streaming/OutputItem.php b/src/Responses/Responses/Streaming/OutputItem.php index 1223c0e3b..68e037452 100644 --- a/src/Responses/Responses/Streaming/OutputItem.php +++ b/src/Responses/Responses/Streaming/OutputItem.php @@ -10,6 +10,7 @@ use OpenAI\Responses\Concerns\HasMetaInformation; use OpenAI\Responses\Meta\MetaInformation; use OpenAI\Responses\Responses\Output\OutputCodeInterpreterToolCall; +use OpenAI\Responses\Responses\Output\OutputCompaction; use OpenAI\Responses\Responses\Output\OutputComputerToolCall; use OpenAI\Responses\Responses\Output\OutputFileSearchToolCall; use OpenAI\Responses\Responses\Output\OutputFunctionToolCall; @@ -34,8 +35,9 @@ * @phpstan-import-type OutputMcpApprovalRequestType from OutputMcpApprovalRequest * @phpstan-import-type OutputMcpCallType from OutputMcpCall * @phpstan-import-type OutputCodeInterpreterToolCallType from OutputCodeInterpreterToolCall + * @phpstan-import-type OutputCompactionType from OutputCompaction * - * @phpstan-type OutputItemType array{type: string, output_index: int, sequence_number: int, item: OutputCodeInterpreterToolCallType|OutputComputerToolCallType|OutputFileSearchToolCallType|OutputFunctionToolCallType|OutputMessageType|OutputReasoningType|OutputWebSearchToolCallType|OutputMcpListToolsType|OutputMcpApprovalRequestType|OutputMcpCallType|OutputImageGenerationToolCallType} + * @phpstan-type OutputItemType array{type: string, output_index: int, sequence_number: int, item: OutputCodeInterpreterToolCallType|OutputComputerToolCallType|OutputFileSearchToolCallType|OutputFunctionToolCallType|OutputMessageType|OutputReasoningType|OutputWebSearchToolCallType|OutputMcpListToolsType|OutputMcpApprovalRequestType|OutputMcpCallType|OutputImageGenerationToolCallType|OutputCompactionType} * * @implements ResponseContract */ @@ -53,7 +55,7 @@ private function __construct( public readonly string $type, public readonly int $outputIndex, public readonly int $sequenceNumber, - public readonly OutputMessage|OutputCodeInterpreterToolCall|OutputFileSearchToolCall|OutputFunctionToolCall|OutputWebSearchToolCall|OutputComputerToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall $item, + public readonly OutputMessage|OutputCodeInterpreterToolCall|OutputFileSearchToolCall|OutputFunctionToolCall|OutputWebSearchToolCall|OutputComputerToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCompaction $item, private readonly MetaInformation $meta, ) {} @@ -74,6 +76,7 @@ public static function from(array $attributes, MetaInformation $meta): self 'mcp_approval_request' => OutputMcpApprovalRequest::from($attributes['item']), 'mcp_call' => OutputMcpCall::from($attributes['item']), 'code_interpreter_call' => OutputCodeInterpreterToolCall::from($attributes['item']), + 'compaction' => OutputCompaction::from($attributes['item']), }; return new self( diff --git a/tests/Fixtures/Responses.php b/tests/Fixtures/Responses.php index 36fd6a2a2..4da44a0f7 100644 --- a/tests/Fixtures/Responses.php +++ b/tests/Fixtures/Responses.php @@ -1042,3 +1042,8 @@ function responseRateLimitsUpdatedEvent() { return fopen(__DIR__.'/Streams/ResponseRateLimitsUpdated.txt', 'r'); } + +function responseOutputItemCompactionDoneEvent() +{ + return fopen(__DIR__.'/Streams/ResponseOutputItemCompactionDone.txt', 'r'); +} diff --git a/tests/Fixtures/Streams/ResponseOutputItemCompactionDone.txt b/tests/Fixtures/Streams/ResponseOutputItemCompactionDone.txt new file mode 100644 index 000000000..c8d409118 --- /dev/null +++ b/tests/Fixtures/Streams/ResponseOutputItemCompactionDone.txt @@ -0,0 +1 @@ +data: {"type":"response.output_item.done","output_index":0,"sequence_number":11,"item":{"id":"cmp_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c","encrypted_content":"encrypted_string_value","type":"compaction","created_by":"user_123"}} diff --git a/tests/Responses/Responses/CreateStreamedResponse.php b/tests/Responses/Responses/CreateStreamedResponse.php index e303d37f7..907243fbd 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\Output\OutputCompaction; +use OpenAI\Responses\Responses\Streaming\OutputItem; use OpenAI\Responses\Responses\Streaming\RateLimits; use OpenAI\Responses\Responses\Streaming\ReasoningTextDelta; use OpenAI\Responses\Responses\Streaming\ReasoningTextDone; @@ -68,3 +70,18 @@ 'type' => 'response.rate_limits.updated', ]); }); + +test('output item done event with compaction item', function () { + $response = CreateStreamedResponse::fake(responseOutputItemCompactionDoneEvent()); + + expect($response->getIterator()->current()) + ->toBeInstanceOf(CreateStreamedResponse::class) + ->event->toBe('response.output_item.done') + ->response->toBeInstanceOf(OutputItem::class) + ->response->outputIndex->toBe(0) + ->response->sequenceNumber->toBe(11) + ->response->item->toBeInstanceOf(OutputCompaction::class) + ->response->item->id->toBe('cmp_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c') + ->response->item->encryptedContent->toBe('encrypted_string_value') + ->response->item->createdBy->toBe('user_123'); +}); From e23409698ad97cf076185d4e85f793f4e72a6d3d Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Mon, 20 Jul 2026 13:56:40 +0400 Subject: [PATCH 2/3] Add programmatic tool calling support --- src/Actions/Conversations/ItemObjects.php | 12 +- src/Actions/Responses/ItemObjects.php | 12 +- src/Actions/Responses/OutputObjects.php | 12 +- src/Actions/Responses/ToolChoiceObjects.php | 2 +- src/Actions/Responses/ToolObjects.php | 9 +- .../Conversations/ConversationItem.php | 4 +- src/Responses/Responses/CreateResponse.php | 7 +- .../Responses/Input/CustomToolCallOutput.php | 14 ++- .../Input/FunctionToolCallOutput.php | 14 ++- src/Responses/Responses/ListInputItems.php | 6 +- .../Responses/Output/OutputCustomToolCall.php | 13 +- .../Output/OutputFunctionToolCall.php | 19 ++- .../Output/OutputFunctionToolCallCaller.php | 54 ++++++++ .../Responses/Output/OutputProgram.php | 63 ++++++++++ .../Responses/Output/OutputProgramOutput.php | 64 ++++++++++ .../Output/OutputToolSearchOutput.php | 3 +- src/Responses/Responses/RetrieveResponse.php | 7 +- .../Responses/Streaming/OutputItem.php | 10 +- .../Responses/Tool/CodeInterpreterTool.php | 10 +- src/Responses/Responses/Tool/CustomTool.php | 6 +- src/Responses/Responses/Tool/FunctionTool.php | 20 ++- .../Tool/NamespaceTools/CustomTool.php | 6 +- .../Tool/NamespaceTools/FunctionTool.php | 20 ++- .../Tool/ProgrammaticToolCallingTool.php | 51 ++++++++ .../Responses/Tool/RemoteMcpTool.php | 13 +- .../Responses/ToolChoice/HostedToolChoice.php | 4 +- tests/Fixtures/Responses.php | 118 ++++++++++++++++++ .../Conversations/ConversationItem.php | 20 +++ .../Conversations/ConversationItemList.php | 15 +++ tests/Responses/Responses/CreateResponse.php | 39 ++++++ .../Responses/CreateStreamedResponse.php | 49 ++++++++ .../Responses/Input/CustomToolCallOutput.php | 3 +- .../Input/FunctionToolCallOutput.php | 19 +++ tests/Responses/Responses/ListInputItems.php | 14 ++- .../Responses/Output/OutputCustomToolCall.php | 5 +- .../Output/OutputFunctionToolCall.php | 23 ++++ .../Responses/Output/OutputProgram.php | 28 +++++ .../Responses/Output/OutputProgramOutput.php | 28 +++++ .../Responses/Tool/CodeInterpreterTool.php | 18 +++ tests/Responses/Responses/Tool/CustomTool.php | 21 ++++ .../Responses/Tool/CustomToolTest.php | 15 +++ .../Responses/Responses/Tool/FunctionTool.php | 19 +++ .../Responses/Tool/NamespaceTool.php | 37 ++++++ .../Tool/ProgrammaticToolCallingTool.php | 24 ++++ .../Responses/Tool/RemoteMcpTool.php | 3 +- 45 files changed, 898 insertions(+), 55 deletions(-) create mode 100644 src/Responses/Responses/Output/OutputFunctionToolCallCaller.php create mode 100644 src/Responses/Responses/Output/OutputProgram.php create mode 100644 src/Responses/Responses/Output/OutputProgramOutput.php create mode 100644 src/Responses/Responses/Tool/ProgrammaticToolCallingTool.php create mode 100644 tests/Responses/Responses/Input/FunctionToolCallOutput.php create mode 100644 tests/Responses/Responses/Output/OutputFunctionToolCall.php create mode 100644 tests/Responses/Responses/Output/OutputProgram.php create mode 100644 tests/Responses/Responses/Output/OutputProgramOutput.php create mode 100644 tests/Responses/Responses/Tool/CodeInterpreterTool.php create mode 100644 tests/Responses/Responses/Tool/CustomTool.php create mode 100644 tests/Responses/Responses/Tool/FunctionTool.php create mode 100644 tests/Responses/Responses/Tool/ProgrammaticToolCallingTool.php diff --git a/src/Actions/Conversations/ItemObjects.php b/src/Actions/Conversations/ItemObjects.php index 4b97552a4..3c0dd0a1d 100644 --- a/src/Actions/Conversations/ItemObjects.php +++ b/src/Actions/Conversations/ItemObjects.php @@ -20,6 +20,8 @@ use OpenAI\Responses\Responses\Output\OutputMcpApprovalRequest; use OpenAI\Responses\Responses\Output\OutputMcpCall; use OpenAI\Responses\Responses\Output\OutputMcpListTools; +use OpenAI\Responses\Responses\Output\OutputProgram; +use OpenAI\Responses\Responses\Output\OutputProgramOutput; use OpenAI\Responses\Responses\Output\OutputReasoning; use OpenAI\Responses\Responses\Output\OutputWebSearchToolCall; @@ -42,10 +44,12 @@ * @phpstan-import-type OutputCodeInterpreterToolCallType from OutputCodeInterpreterToolCall * @phpstan-import-type OutputLocalShellCallType from OutputLocalShellCall * @phpstan-import-type OutputCustomToolCallType from OutputCustomToolCall + * @phpstan-import-type OutputProgramType from OutputProgram + * @phpstan-import-type OutputProgramOutputType from OutputProgramOutput * - * @phpstan-type ItemObjectTypes MessageType|ComputerToolCallOutputType|FunctionToolCallOutputType|LocalShellCallOutputType|McpApprovalResponseType|CustomToolCallOutputType|OutputComputerToolCallType|OutputFileSearchToolCallType|OutputFunctionToolCallType|OutputReasoningType|OutputWebSearchToolCallType|OutputMcpListToolsType|OutputMcpApprovalRequestType|OutputMcpCallType|OutputImageGenerationToolCallType|OutputCodeInterpreterToolCallType|OutputLocalShellCallType|OutputCustomToolCallType + * @phpstan-type ItemObjectTypes MessageType|ComputerToolCallOutputType|FunctionToolCallOutputType|LocalShellCallOutputType|McpApprovalResponseType|CustomToolCallOutputType|OutputComputerToolCallType|OutputFileSearchToolCallType|OutputFunctionToolCallType|OutputReasoningType|OutputWebSearchToolCallType|OutputMcpListToolsType|OutputMcpApprovalRequestType|OutputMcpCallType|OutputImageGenerationToolCallType|OutputCodeInterpreterToolCallType|OutputLocalShellCallType|OutputCustomToolCallType|OutputProgramType|OutputProgramOutputType * @phpstan-type ConversationItemObjectTypes array - * @phpstan-type ConversationItemObjectReturnType array + * @phpstan-type ConversationItemObjectReturnType array */ final class ItemObjects { @@ -56,7 +60,7 @@ final class ItemObjects public static function parse(array $outputItems): array { return array_map( - fn (array $item): Message|ComputerToolCallOutput|FunctionToolCallOutput|LocalShellCallOutput|McpApprovalResponse|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall|OutputLocalShellCall|OutputCustomToolCall|CustomToolCallOutput => match ($item['type']) { + fn (array $item): Message|ComputerToolCallOutput|FunctionToolCallOutput|LocalShellCallOutput|McpApprovalResponse|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall|OutputLocalShellCall|OutputCustomToolCall|CustomToolCallOutput|OutputProgram|OutputProgramOutput => match ($item['type']) { 'message' => Message::from($item), 'file_search_call' => OutputFileSearchToolCall::from($item), 'function_call' => OutputFunctionToolCall::from($item), @@ -75,6 +79,8 @@ public static function parse(array $outputItems): array 'local_shell_call_output' => LocalShellCallOutput::from($item), 'custom_tool_call_output' => CustomToolCallOutput::from($item), 'mcp_approval_response' => McpApprovalResponse::from($item), + 'program' => OutputProgram::from($item), + 'program_output' => OutputProgramOutput::from($item), }, $outputItems, ); diff --git a/src/Actions/Responses/ItemObjects.php b/src/Actions/Responses/ItemObjects.php index 18e23ba5d..e7bdb74ff 100644 --- a/src/Actions/Responses/ItemObjects.php +++ b/src/Actions/Responses/ItemObjects.php @@ -21,6 +21,8 @@ use OpenAI\Responses\Responses\Output\OutputMcpCall; use OpenAI\Responses\Responses\Output\OutputMcpListTools; use OpenAI\Responses\Responses\Output\OutputMessage; +use OpenAI\Responses\Responses\Output\OutputProgram; +use OpenAI\Responses\Responses\Output\OutputProgramOutput; use OpenAI\Responses\Responses\Output\OutputReasoning; use OpenAI\Responses\Responses\Output\OutputWebSearchToolCall; @@ -44,9 +46,11 @@ * @phpstan-import-type OutputCodeInterpreterToolCallType from OutputCodeInterpreterToolCall * @phpstan-import-type OutputLocalShellCallType from OutputLocalShellCall * @phpstan-import-type OutputCustomToolCallType from OutputCustomToolCall + * @phpstan-import-type OutputProgramType from OutputProgram + * @phpstan-import-type OutputProgramOutputType from OutputProgramOutput * - * @phpstan-type ResponseItemObjectTypes array - * @phpstan-type ResponseItemObjectReturnType array + * @phpstan-type ResponseItemObjectTypes array + * @phpstan-type ResponseItemObjectReturnType array */ final class ItemObjects { @@ -57,7 +61,7 @@ final class ItemObjects public static function parse(array $outputItems): array { return array_map( - fn (array $item): InputMessage|ComputerToolCallOutput|FunctionToolCallOutput|LocalShellCallOutput|McpApprovalResponse|CustomToolCallOutput|OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall|OutputLocalShellCall|OutputCustomToolCall => match ($item['type']) { + fn (array $item): InputMessage|ComputerToolCallOutput|FunctionToolCallOutput|LocalShellCallOutput|McpApprovalResponse|CustomToolCallOutput|OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall|OutputLocalShellCall|OutputCustomToolCall|OutputProgram|OutputProgramOutput => match ($item['type']) { 'message' => $item['role'] === 'assistant' ? OutputMessage::from($item) : InputMessage::from($item), 'file_search_call' => OutputFileSearchToolCall::from($item), 'function_call' => OutputFunctionToolCall::from($item), @@ -76,6 +80,8 @@ public static function parse(array $outputItems): array 'local_shell_call_output' => LocalShellCallOutput::from($item), 'custom_tool_call_output' => CustomToolCallOutput::from($item), 'mcp_approval_response' => McpApprovalResponse::from($item), + 'program' => OutputProgram::from($item), + 'program_output' => OutputProgramOutput::from($item), }, $outputItems, ); diff --git a/src/Actions/Responses/OutputObjects.php b/src/Actions/Responses/OutputObjects.php index de7955d58..0e4477462 100644 --- a/src/Actions/Responses/OutputObjects.php +++ b/src/Actions/Responses/OutputObjects.php @@ -16,6 +16,8 @@ use OpenAI\Responses\Responses\Output\OutputMcpCall; use OpenAI\Responses\Responses\Output\OutputMcpListTools; use OpenAI\Responses\Responses\Output\OutputMessage; +use OpenAI\Responses\Responses\Output\OutputProgram; +use OpenAI\Responses\Responses\Output\OutputProgramOutput; use OpenAI\Responses\Responses\Output\OutputReasoning; use OpenAI\Responses\Responses\Output\OutputToolSearchCall; use OpenAI\Responses\Responses\Output\OutputToolSearchOutput; @@ -34,13 +36,15 @@ * @phpstan-import-type OutputImageGenerationToolCallType from OutputImageGenerationToolCall * @phpstan-import-type OutputCodeInterpreterToolCallType from OutputCodeInterpreterToolCall * @phpstan-import-type OutputCompactionType from OutputCompaction + * @phpstan-import-type OutputProgramType from OutputProgram + * @phpstan-import-type OutputProgramOutputType from OutputProgramOutput * @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 { @@ -51,10 +55,12 @@ 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|OutputToolSearchCall|OutputToolSearchOutput|OutputCompaction => match ($item['type']) { + fn (array $item): OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputProgram|OutputProgramOutput|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall|OutputLocalShellCall|OutputCustomToolCall|OutputToolSearchCall|OutputToolSearchOutput|OutputCompaction => match ($item['type']) { 'message' => OutputMessage::from($item), 'file_search_call' => OutputFileSearchToolCall::from($item), 'function_call' => OutputFunctionToolCall::from($item), + 'program' => OutputProgram::from($item), + 'program_output' => OutputProgramOutput::from($item), 'web_search_call' => OutputWebSearchToolCall::from($item), 'computer_call' => OutputComputerToolCall::from($item), 'reasoning' => OutputReasoning::from($item), diff --git a/src/Actions/Responses/ToolChoiceObjects.php b/src/Actions/Responses/ToolChoiceObjects.php index 9dc52cf78..553e97aa7 100644 --- a/src/Actions/Responses/ToolChoiceObjects.php +++ b/src/Actions/Responses/ToolChoiceObjects.php @@ -24,7 +24,7 @@ public static function parse(array|string $toolChoice): string|FunctionToolChoic { return is_array($toolChoice) ? match ($toolChoice['type']) { - 'file_search', 'web_search', 'web_search_preview', 'computer_use_preview' => HostedToolChoice::from($toolChoice), + 'file_search', 'web_search', 'web_search_preview', 'computer_use_preview', 'programmatic_tool_calling' => HostedToolChoice::from($toolChoice), 'function' => FunctionToolChoice::from($toolChoice), } : $toolChoice; diff --git a/src/Actions/Responses/ToolObjects.php b/src/Actions/Responses/ToolObjects.php index a99c8a905..bb6c8035d 100644 --- a/src/Actions/Responses/ToolObjects.php +++ b/src/Actions/Responses/ToolObjects.php @@ -11,6 +11,7 @@ use OpenAI\Responses\Responses\Tool\FunctionTool; use OpenAI\Responses\Responses\Tool\ImageGenerationTool; use OpenAI\Responses\Responses\Tool\NamespaceTool; +use OpenAI\Responses\Responses\Tool\ProgrammaticToolCallingTool; use OpenAI\Responses\Responses\Tool\RemoteMcpTool; use OpenAI\Responses\Responses\Tool\ToolSearchTool; use OpenAI\Responses\Responses\Tool\WebSearchTool; @@ -23,12 +24,13 @@ * @phpstan-import-type FunctionToolType from FunctionTool * @phpstan-import-type ImageGenerationToolType from ImageGenerationTool * @phpstan-import-type NamespaceToolType from NamespaceTool + * @phpstan-import-type ProgrammaticToolCallingToolType from ProgrammaticToolCallingTool * @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 + * @phpstan-type ResponseToolObjectTypes array + * @phpstan-type ResponseToolObjectReturnType array */ final class ToolObjects { @@ -39,7 +41,7 @@ final class ToolObjects public static function parse(array $toolItems): array { return array_map( - fn (array $tool): CodeInterpreterTool|ComputerUseTool|CustomTool|FileSearchTool|FunctionTool|ImageGenerationTool|NamespaceTool|RemoteMcpTool|ToolSearchTool|WebSearchTool => match ($tool['type']) { + fn (array $tool): CodeInterpreterTool|ComputerUseTool|CustomTool|FileSearchTool|FunctionTool|ImageGenerationTool|NamespaceTool|ProgrammaticToolCallingTool|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), @@ -50,6 +52,7 @@ public static function parse(array $toolItems): array 'tool_search' => ToolSearchTool::from($tool), 'namespace' => NamespaceTool::from($tool), 'custom' => CustomTool::from($tool), + 'programmatic_tool_calling' => ProgrammaticToolCallingTool::from($tool), }, $toolItems, ); diff --git a/src/Responses/Conversations/ConversationItem.php b/src/Responses/Conversations/ConversationItem.php index 62ce93f5e..140b7c4f1 100644 --- a/src/Responses/Conversations/ConversationItem.php +++ b/src/Responses/Conversations/ConversationItem.php @@ -23,6 +23,8 @@ use OpenAI\Responses\Responses\Output\OutputMcpApprovalRequest; use OpenAI\Responses\Responses\Output\OutputMcpCall; use OpenAI\Responses\Responses\Output\OutputMcpListTools; +use OpenAI\Responses\Responses\Output\OutputProgram; +use OpenAI\Responses\Responses\Output\OutputProgramOutput; use OpenAI\Responses\Responses\Output\OutputReasoning; use OpenAI\Responses\Responses\Output\OutputWebSearchToolCall; use OpenAI\Testing\Responses\Concerns\Fakeable; @@ -44,7 +46,7 @@ final class ConversationItem implements ResponseContract use Fakeable; private function __construct( - public readonly Message|OutputFileSearchToolCall|OutputFunctionToolCall|FunctionToolCallOutput|LocalShellCallOutput|McpApprovalResponse|CustomToolCallOutput|OutputWebSearchToolCall|OutputComputerToolCall|ComputerToolCallOutput|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall|OutputLocalShellCall|OutputCustomToolCall $item + public readonly Message|OutputFileSearchToolCall|OutputFunctionToolCall|FunctionToolCallOutput|LocalShellCallOutput|McpApprovalResponse|CustomToolCallOutput|OutputWebSearchToolCall|OutputComputerToolCall|ComputerToolCallOutput|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall|OutputLocalShellCall|OutputCustomToolCall|OutputProgram|OutputProgramOutput $item ) {} /** diff --git a/src/Responses/Responses/CreateResponse.php b/src/Responses/Responses/CreateResponse.php index 9078a76fa..e3ac63acf 100644 --- a/src/Responses/Responses/CreateResponse.php +++ b/src/Responses/Responses/CreateResponse.php @@ -25,6 +25,8 @@ use OpenAI\Responses\Responses\Output\OutputMcpCall; use OpenAI\Responses\Responses\Output\OutputMcpListTools; use OpenAI\Responses\Responses\Output\OutputMessage; +use OpenAI\Responses\Responses\Output\OutputProgram; +use OpenAI\Responses\Responses\Output\OutputProgramOutput; use OpenAI\Responses\Responses\Output\OutputReasoning; use OpenAI\Responses\Responses\Output\OutputToolSearchCall; use OpenAI\Responses\Responses\Output\OutputToolSearchOutput; @@ -36,6 +38,7 @@ use OpenAI\Responses\Responses\Tool\FunctionTool; use OpenAI\Responses\Responses\Tool\ImageGenerationTool; use OpenAI\Responses\Responses\Tool\NamespaceTool; +use OpenAI\Responses\Responses\Tool\ProgrammaticToolCallingTool; use OpenAI\Responses\Responses\Tool\RemoteMcpTool; use OpenAI\Responses\Responses\Tool\ToolSearchTool; use OpenAI\Responses\Responses\Tool\WebSearchTool; @@ -196,7 +199,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|OutputToolSearchCall|OutputToolSearchOutput|OutputCompaction $output): array => $output->toArray(), + fn (OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputProgram|OutputProgramOutput|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall|OutputLocalShellCall|OutputCustomToolCall|OutputToolSearchCall|OutputToolSearchOutput|OutputCompaction $output): array => $output->toArray(), $this->output ), 'parallel_tool_calls' => $this->parallelToolCalls, @@ -213,7 +216,7 @@ public function toArray(): array ? $this->toolChoice : $this->toolChoice->toArray(), 'tools' => array_map( - fn (ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool|ToolSearchTool|NamespaceTool|CustomTool $tool): array => $tool->toArray(), + fn (ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool|ToolSearchTool|NamespaceTool|CustomTool|ProgrammaticToolCallingTool $tool): array => $tool->toArray(), $this->tools ), 'top_logprobs' => $this->topLogProbs, diff --git a/src/Responses/Responses/Input/CustomToolCallOutput.php b/src/Responses/Responses/Input/CustomToolCallOutput.php index 3adf9bf51..4ee2c48bd 100644 --- a/src/Responses/Responses/Input/CustomToolCallOutput.php +++ b/src/Responses/Responses/Input/CustomToolCallOutput.php @@ -6,10 +6,13 @@ use OpenAI\Contracts\ResponseContract; use OpenAI\Responses\Concerns\ArrayAccessible; +use OpenAI\Responses\Responses\Output\OutputFunctionToolCallCaller; use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @phpstan-type CustomToolCallOutputType array{call_id: string, output: string, type: 'custom_tool_call_output', id: string} + * @phpstan-import-type OutputFunctionToolCallCallerType from OutputFunctionToolCallCaller + * + * @phpstan-type CustomToolCallOutputType array{call_id: string, output: string, type: 'custom_tool_call_output', id: string, caller?: OutputFunctionToolCallCallerType|null} * * @implements ResponseContract */ @@ -30,6 +33,7 @@ private function __construct( public readonly string $output, public readonly string $type, public readonly string $id, + public readonly ?OutputFunctionToolCallCaller $caller, ) {} /** @@ -42,6 +46,9 @@ public static function from(array $attributes): self output: $attributes['output'], type: $attributes['type'], id: $attributes['id'], + caller: isset($attributes['caller']) + ? OutputFunctionToolCallCaller::from($attributes['caller']) + : null, ); } @@ -50,11 +57,12 @@ public static function from(array $attributes): self */ public function toArray(): array { - return [ + return array_filter([ 'type' => $this->type, 'call_id' => $this->callId, 'id' => $this->id, 'output' => $this->output, - ]; + 'caller' => $this->caller?->toArray(), + ], fn (mixed $value): bool => $value !== null); } } diff --git a/src/Responses/Responses/Input/FunctionToolCallOutput.php b/src/Responses/Responses/Input/FunctionToolCallOutput.php index 267e7232c..31aa6c745 100644 --- a/src/Responses/Responses/Input/FunctionToolCallOutput.php +++ b/src/Responses/Responses/Input/FunctionToolCallOutput.php @@ -6,10 +6,13 @@ use OpenAI\Contracts\ResponseContract; use OpenAI\Responses\Concerns\ArrayAccessible; +use OpenAI\Responses\Responses\Output\OutputFunctionToolCallCaller; use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @phpstan-type FunctionToolCallOutputType array{call_id: string, id: string, output: string, type: 'function_call_output', status: 'in_progress'|'completed'|'incompleted'} + * @phpstan-import-type OutputFunctionToolCallCallerType from OutputFunctionToolCallCaller + * + * @phpstan-type FunctionToolCallOutputType array{call_id: string, id: string, output: string, type: 'function_call_output', status: 'in_progress'|'completed'|'incompleted', caller?: OutputFunctionToolCallCallerType|null} * * @implements ResponseContract */ @@ -32,6 +35,7 @@ private function __construct( public readonly string $output, public readonly string $type, public readonly string $status, + public readonly ?OutputFunctionToolCallCaller $caller, ) {} /** @@ -45,6 +49,9 @@ public static function from(array $attributes): self output: $attributes['output'], type: $attributes['type'], status: $attributes['status'], + caller: isset($attributes['caller']) + ? OutputFunctionToolCallCaller::from($attributes['caller']) + : null, ); } @@ -53,12 +60,13 @@ public static function from(array $attributes): self */ public function toArray(): array { - return [ + return array_filter([ 'call_id' => $this->callId, 'id' => $this->id, 'output' => $this->output, 'type' => $this->type, 'status' => $this->status, - ]; + 'caller' => $this->caller?->toArray(), + ], fn (mixed $value): bool => $value !== null); } } diff --git a/src/Responses/Responses/ListInputItems.php b/src/Responses/Responses/ListInputItems.php index f60f950e0..ed7682d4a 100644 --- a/src/Responses/Responses/ListInputItems.php +++ b/src/Responses/Responses/ListInputItems.php @@ -27,6 +27,8 @@ use OpenAI\Responses\Responses\Output\OutputMcpCall; use OpenAI\Responses\Responses\Output\OutputMcpListTools; use OpenAI\Responses\Responses\Output\OutputMessage; +use OpenAI\Responses\Responses\Output\OutputProgram; +use OpenAI\Responses\Responses\Output\OutputProgramOutput; use OpenAI\Responses\Responses\Output\OutputReasoning; use OpenAI\Responses\Responses\Output\OutputWebSearchToolCall; use OpenAI\Testing\Responses\Concerns\Fakeable; @@ -47,7 +49,7 @@ final class ListInputItems implements ResponseContract, ResponseHasMetaInformati use HasMetaInformation; /** - * @param array $data + * @param array $data * @param 'list' $object */ private function __construct( @@ -84,7 +86,7 @@ public function toArray(): array return [ 'object' => $this->object, 'data' => array_map( - fn (InputMessage|OutputMessage|OutputFileSearchToolCall|OutputFunctionToolCall|FunctionToolCallOutput|OutputWebSearchToolCall|OutputComputerToolCall|ComputerToolCallOutput|LocalShellCallOutput|McpApprovalResponse|CustomToolCallOutput|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall|OutputLocalShellCall|OutputCustomToolCall $item): array => $item->toArray(), + fn (InputMessage|OutputMessage|OutputFileSearchToolCall|OutputFunctionToolCall|FunctionToolCallOutput|OutputWebSearchToolCall|OutputComputerToolCall|ComputerToolCallOutput|LocalShellCallOutput|McpApprovalResponse|CustomToolCallOutput|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall|OutputLocalShellCall|OutputCustomToolCall|OutputProgram|OutputProgramOutput $item): array => $item->toArray(), $this->data, ), 'first_id' => $this->firstId, diff --git a/src/Responses/Responses/Output/OutputCustomToolCall.php b/src/Responses/Responses/Output/OutputCustomToolCall.php index 734a65855..de475db61 100644 --- a/src/Responses/Responses/Output/OutputCustomToolCall.php +++ b/src/Responses/Responses/Output/OutputCustomToolCall.php @@ -9,7 +9,9 @@ use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @phpstan-type OutputCustomToolCallType array{call_id: string, input: string, name: string, type: 'custom_tool_call', id: string} + * @phpstan-import-type OutputFunctionToolCallCallerType from OutputFunctionToolCallCaller + * + * @phpstan-type OutputCustomToolCallType array{call_id: string, input: string, name: string, type: 'custom_tool_call', id: string, caller?: OutputFunctionToolCallCallerType|null} * * @implements ResponseContract */ @@ -31,6 +33,7 @@ private function __construct( public readonly string $name, public readonly string $id, public readonly string $type, + public readonly ?OutputFunctionToolCallCaller $caller, ) {} /** @@ -44,6 +47,9 @@ public static function from(array $attributes): self name: $attributes['name'], id: $attributes['id'], type: $attributes['type'], + caller: isset($attributes['caller']) + ? OutputFunctionToolCallCaller::from($attributes['caller']) + : null, ); } @@ -52,12 +58,13 @@ public static function from(array $attributes): self */ public function toArray(): array { - return [ + return array_filter([ 'type' => $this->type, 'call_id' => $this->callId, 'input' => $this->input, 'name' => $this->name, 'id' => $this->id, - ]; + 'caller' => $this->caller?->toArray(), + ], fn (mixed $value): bool => $value !== null); } } diff --git a/src/Responses/Responses/Output/OutputFunctionToolCall.php b/src/Responses/Responses/Output/OutputFunctionToolCall.php index 985fa9d26..e8ce70169 100644 --- a/src/Responses/Responses/Output/OutputFunctionToolCall.php +++ b/src/Responses/Responses/Output/OutputFunctionToolCall.php @@ -9,7 +9,9 @@ use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @phpstan-type OutputFunctionToolCallType array{arguments: string, call_id: string, name: string, type: 'function_call', id: string, status: 'in_progress'|'completed'|'incomplete'} + * @phpstan-import-type OutputFunctionToolCallCallerType from OutputFunctionToolCallCaller + * + * @phpstan-type OutputFunctionToolCallType array{arguments: string, call_id: string, name: string, type: 'function_call', id: string, status?: 'in_progress'|'completed'|'incomplete', caller?: OutputFunctionToolCallCallerType|null} * * @implements ResponseContract */ @@ -24,7 +26,7 @@ final class OutputFunctionToolCall implements ResponseContract /** * @param 'function_call' $type - * @param 'in_progress'|'completed'|'incomplete' $status + * @param 'in_progress'|'completed'|'incomplete'|null $status */ private function __construct( public readonly string $arguments, @@ -32,7 +34,8 @@ private function __construct( public readonly string $name, public readonly string $type, public readonly string $id, - public readonly string $status, + public readonly ?string $status, + public readonly ?OutputFunctionToolCallCaller $caller, ) {} /** @@ -46,7 +49,10 @@ public static function from(array $attributes): self name: $attributes['name'], type: $attributes['type'], id: $attributes['id'], - status: $attributes['status'], + status: $attributes['status'] ?? null, + caller: isset($attributes['caller']) + ? OutputFunctionToolCallCaller::from($attributes['caller']) + : null, ); } @@ -55,13 +61,14 @@ public static function from(array $attributes): self */ public function toArray(): array { - return [ + return array_filter([ 'arguments' => $this->arguments, 'call_id' => $this->callId, 'name' => $this->name, 'type' => $this->type, 'id' => $this->id, 'status' => $this->status, - ]; + 'caller' => $this->caller?->toArray(), + ], fn (mixed $value): bool => $value !== null); } } diff --git a/src/Responses/Responses/Output/OutputFunctionToolCallCaller.php b/src/Responses/Responses/Output/OutputFunctionToolCallCaller.php new file mode 100644 index 000000000..04130f8a1 --- /dev/null +++ b/src/Responses/Responses/Output/OutputFunctionToolCallCaller.php @@ -0,0 +1,54 @@ + + */ +final class OutputFunctionToolCallCaller implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'direct'|'program' $type + */ + private function __construct( + public readonly string $type, + public readonly ?string $callerId, + ) {} + + /** + * @param OutputFunctionToolCallCallerType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + callerId: $attributes['caller_id'] ?? null, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return array_filter([ + 'type' => $this->type, + 'caller_id' => $this->callerId, + ], fn (mixed $value): bool => $value !== null); + } +} diff --git a/src/Responses/Responses/Output/OutputProgram.php b/src/Responses/Responses/Output/OutputProgram.php new file mode 100644 index 000000000..acd73273f --- /dev/null +++ b/src/Responses/Responses/Output/OutputProgram.php @@ -0,0 +1,63 @@ + + */ +final class OutputProgram implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'program' $type + */ + private function __construct( + public readonly string $id, + public readonly string $callId, + public readonly string $code, + public readonly string $fingerprint, + public readonly string $type, + ) {} + + /** + * @param OutputProgramType $attributes + */ + public static function from(array $attributes): self + { + return new self( + id: $attributes['id'], + callId: $attributes['call_id'], + code: $attributes['code'], + fingerprint: $attributes['fingerprint'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'id' => $this->id, + 'call_id' => $this->callId, + 'code' => $this->code, + 'fingerprint' => $this->fingerprint, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputProgramOutput.php b/src/Responses/Responses/Output/OutputProgramOutput.php new file mode 100644 index 000000000..9ff8d6c60 --- /dev/null +++ b/src/Responses/Responses/Output/OutputProgramOutput.php @@ -0,0 +1,64 @@ + + */ +final class OutputProgramOutput implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'completed'|'incomplete' $status + * @param 'program_output' $type + */ + private function __construct( + public readonly string $id, + public readonly string $callId, + public readonly string $result, + public readonly string $status, + public readonly string $type, + ) {} + + /** + * @param OutputProgramOutputType $attributes + */ + public static function from(array $attributes): self + { + return new self( + id: $attributes['id'], + callId: $attributes['call_id'], + result: $attributes['result'], + status: $attributes['status'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'id' => $this->id, + 'call_id' => $this->callId, + 'result' => $this->result, + 'status' => $this->status, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputToolSearchOutput.php b/src/Responses/Responses/Output/OutputToolSearchOutput.php index 8308530ff..0e4108351 100644 --- a/src/Responses/Responses/Output/OutputToolSearchOutput.php +++ b/src/Responses/Responses/Output/OutputToolSearchOutput.php @@ -14,6 +14,7 @@ use OpenAI\Responses\Responses\Tool\FunctionTool; use OpenAI\Responses\Responses\Tool\ImageGenerationTool; use OpenAI\Responses\Responses\Tool\NamespaceTool; +use OpenAI\Responses\Responses\Tool\ProgrammaticToolCallingTool; use OpenAI\Responses\Responses\Tool\RemoteMcpTool; use OpenAI\Responses\Responses\Tool\ToolSearchTool; use OpenAI\Responses\Responses\Tool\WebSearchTool; @@ -79,7 +80,7 @@ public function toArray(): array 'execution' => $this->execution, 'status' => $this->status, 'tools' => array_map( - fn (CodeInterpreterTool|ComputerUseTool|CustomTool|FileSearchTool|FunctionTool|ImageGenerationTool|NamespaceTool|RemoteMcpTool|ToolSearchTool|WebSearchTool $tool): array => $tool->toArray(), + fn (CodeInterpreterTool|ComputerUseTool|CustomTool|FileSearchTool|FunctionTool|ImageGenerationTool|NamespaceTool|ProgrammaticToolCallingTool|RemoteMcpTool|ToolSearchTool|WebSearchTool $tool): array => $tool->toArray(), $this->tools, ), 'type' => $this->type, diff --git a/src/Responses/Responses/RetrieveResponse.php b/src/Responses/Responses/RetrieveResponse.php index aab18991d..12df04cef 100644 --- a/src/Responses/Responses/RetrieveResponse.php +++ b/src/Responses/Responses/RetrieveResponse.php @@ -25,6 +25,8 @@ use OpenAI\Responses\Responses\Output\OutputMcpCall; use OpenAI\Responses\Responses\Output\OutputMcpListTools; use OpenAI\Responses\Responses\Output\OutputMessage; +use OpenAI\Responses\Responses\Output\OutputProgram; +use OpenAI\Responses\Responses\Output\OutputProgramOutput; use OpenAI\Responses\Responses\Output\OutputReasoning; use OpenAI\Responses\Responses\Output\OutputToolSearchCall; use OpenAI\Responses\Responses\Output\OutputToolSearchOutput; @@ -36,6 +38,7 @@ use OpenAI\Responses\Responses\Tool\FunctionTool; use OpenAI\Responses\Responses\Tool\ImageGenerationTool; use OpenAI\Responses\Responses\Tool\NamespaceTool; +use OpenAI\Responses\Responses\Tool\ProgrammaticToolCallingTool; use OpenAI\Responses\Responses\Tool\RemoteMcpTool; use OpenAI\Responses\Responses\Tool\ToolSearchTool; use OpenAI\Responses\Responses\Tool\WebSearchTool; @@ -196,7 +199,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|OutputToolSearchCall|OutputToolSearchOutput|OutputCompaction $output): array => $output->toArray(), + fn (OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputProgram|OutputProgramOutput|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCodeInterpreterToolCall|OutputLocalShellCall|OutputCustomToolCall|OutputToolSearchCall|OutputToolSearchOutput|OutputCompaction $output): array => $output->toArray(), $this->output ), 'output_text' => $this->outputText, @@ -214,7 +217,7 @@ public function toArray(): array ? $this->toolChoice : $this->toolChoice->toArray(), 'tools' => array_map( - fn (ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool|ToolSearchTool|NamespaceTool|CustomTool $tool): array => $tool->toArray(), + fn (ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool|ToolSearchTool|NamespaceTool|CustomTool|ProgrammaticToolCallingTool $tool): array => $tool->toArray(), $this->tools ), 'top_logprobs' => $this->topLogProbs, diff --git a/src/Responses/Responses/Streaming/OutputItem.php b/src/Responses/Responses/Streaming/OutputItem.php index 68e037452..52159f4cf 100644 --- a/src/Responses/Responses/Streaming/OutputItem.php +++ b/src/Responses/Responses/Streaming/OutputItem.php @@ -19,6 +19,8 @@ use OpenAI\Responses\Responses\Output\OutputMcpCall; use OpenAI\Responses\Responses\Output\OutputMcpListTools; use OpenAI\Responses\Responses\Output\OutputMessage; +use OpenAI\Responses\Responses\Output\OutputProgram; +use OpenAI\Responses\Responses\Output\OutputProgramOutput; use OpenAI\Responses\Responses\Output\OutputReasoning; use OpenAI\Responses\Responses\Output\OutputWebSearchToolCall; use OpenAI\Testing\Responses\Concerns\Fakeable; @@ -36,8 +38,10 @@ * @phpstan-import-type OutputMcpCallType from OutputMcpCall * @phpstan-import-type OutputCodeInterpreterToolCallType from OutputCodeInterpreterToolCall * @phpstan-import-type OutputCompactionType from OutputCompaction + * @phpstan-import-type OutputProgramType from OutputProgram + * @phpstan-import-type OutputProgramOutputType from OutputProgramOutput * - * @phpstan-type OutputItemType array{type: string, output_index: int, sequence_number: int, item: OutputCodeInterpreterToolCallType|OutputComputerToolCallType|OutputFileSearchToolCallType|OutputFunctionToolCallType|OutputMessageType|OutputReasoningType|OutputWebSearchToolCallType|OutputMcpListToolsType|OutputMcpApprovalRequestType|OutputMcpCallType|OutputImageGenerationToolCallType|OutputCompactionType} + * @phpstan-type OutputItemType array{type: string, output_index: int, sequence_number: int, item: OutputCodeInterpreterToolCallType|OutputComputerToolCallType|OutputFileSearchToolCallType|OutputFunctionToolCallType|OutputMessageType|OutputProgramType|OutputProgramOutputType|OutputReasoningType|OutputWebSearchToolCallType|OutputMcpListToolsType|OutputMcpApprovalRequestType|OutputMcpCallType|OutputImageGenerationToolCallType|OutputCompactionType} * * @implements ResponseContract */ @@ -55,7 +59,7 @@ private function __construct( public readonly string $type, public readonly int $outputIndex, public readonly int $sequenceNumber, - public readonly OutputMessage|OutputCodeInterpreterToolCall|OutputFileSearchToolCall|OutputFunctionToolCall|OutputWebSearchToolCall|OutputComputerToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCompaction $item, + public readonly OutputMessage|OutputCodeInterpreterToolCall|OutputFileSearchToolCall|OutputFunctionToolCall|OutputProgram|OutputProgramOutput|OutputWebSearchToolCall|OutputComputerToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall|OutputCompaction $item, private readonly MetaInformation $meta, ) {} @@ -68,6 +72,8 @@ public static function from(array $attributes, MetaInformation $meta): self 'message' => OutputMessage::from($attributes['item']), 'file_search_call' => OutputFileSearchToolCall::from($attributes['item']), 'function_call' => OutputFunctionToolCall::from($attributes['item']), + 'program' => OutputProgram::from($attributes['item']), + 'program_output' => OutputProgramOutput::from($attributes['item']), 'web_search_call' => OutputWebSearchToolCall::from($attributes['item']), 'computer_call' => OutputComputerToolCall::from($attributes['item']), 'reasoning' => OutputReasoning::from($attributes['item']), diff --git a/src/Responses/Responses/Tool/CodeInterpreterTool.php b/src/Responses/Responses/Tool/CodeInterpreterTool.php index 5716adf01..3df57ae55 100644 --- a/src/Responses/Responses/Tool/CodeInterpreterTool.php +++ b/src/Responses/Responses/Tool/CodeInterpreterTool.php @@ -11,7 +11,7 @@ /** * @phpstan-import-type CodeInterpreterContainerAutoType from CodeInterpreterContainerAuto * - * @phpstan-type CodeInterpreterToolType array{container: string|CodeInterpreterContainerAutoType, type: 'code_interpreter'} + * @phpstan-type CodeInterpreterToolType array{container: string|CodeInterpreterContainerAutoType, type: 'code_interpreter', allowed_callers?: array|null} * * @implements ResponseContract */ @@ -26,10 +26,12 @@ final class CodeInterpreterTool implements ResponseContract /** * @param 'code_interpreter' $type + * @param array|null $allowedCallers */ private function __construct( public readonly string|CodeInterpreterContainerAuto $container, public readonly string $type, + public readonly ?array $allowedCallers = null, ) {} /** @@ -42,6 +44,7 @@ public static function from(array $attributes): self ? $attributes['container'] : CodeInterpreterContainerAuto::from($attributes['container']), type: $attributes['type'], + allowedCallers: $attributes['allowed_callers'] ?? null, ); } @@ -50,11 +53,12 @@ public static function from(array $attributes): self */ public function toArray(): array { - return [ + return array_filter([ 'container' => $this->container instanceof CodeInterpreterContainerAuto ? $this->container->toArray() : $this->container, 'type' => $this->type, - ]; + 'allowed_callers' => $this->allowedCallers, + ], fn (mixed $value): bool => $value !== null); } } diff --git a/src/Responses/Responses/Tool/CustomTool.php b/src/Responses/Responses/Tool/CustomTool.php index d670cc2b6..3ecf45155 100644 --- a/src/Responses/Responses/Tool/CustomTool.php +++ b/src/Responses/Responses/Tool/CustomTool.php @@ -14,7 +14,7 @@ /** * @phpstan-import-type CustomToolInputTypes from CustomToolInputObjects * - * @phpstan-type CustomToolType array{name: string, type: 'custom', defer_loading?: bool, description?: string, format?: CustomToolInputTypes} + * @phpstan-type CustomToolType array{name: string, type: 'custom', defer_loading?: bool, description?: string, format?: CustomToolInputTypes, allowed_callers?: array|null} * * @implements ResponseContract */ @@ -29,6 +29,7 @@ final class CustomTool implements ResponseContract /** * @param 'custom' $type + * @param array|null $allowedCallers */ private function __construct( public readonly string $name, @@ -36,6 +37,7 @@ private function __construct( public readonly ?bool $deferLoading = null, public readonly ?string $description = null, public readonly TextInput|GrammarInput|null $format = null, + public readonly ?array $allowedCallers = null, ) {} /** @@ -51,6 +53,7 @@ public static function from(array $attributes): self format: isset($attributes['format']) ? CustomToolInputObjects::parse($attributes['format']) : null, + allowedCallers: $attributes['allowed_callers'] ?? null, ); } @@ -65,6 +68,7 @@ public function toArray(): array 'defer_loading' => $this->deferLoading, 'description' => $this->description, 'format' => $this->format?->toArray(), + 'allowed_callers' => $this->allowedCallers, ], fn (mixed $value): bool => $value !== null); } } diff --git a/src/Responses/Responses/Tool/FunctionTool.php b/src/Responses/Responses/Tool/FunctionTool.php index b7b6e86d3..ec823a546 100644 --- a/src/Responses/Responses/Tool/FunctionTool.php +++ b/src/Responses/Responses/Tool/FunctionTool.php @@ -9,7 +9,7 @@ use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @phpstan-type FunctionToolType array{name: string, parameters: array, strict: bool, type: 'function', description: ?string} + * @phpstan-type FunctionToolType array{name: string, parameters: array, strict: bool, type: 'function', description: ?string, allowed_callers?: array|null, output_schema?: array|null} * * @implements ResponseContract */ @@ -25,6 +25,8 @@ final class FunctionTool implements ResponseContract /** * @param array $parameters * @param 'function' $type + * @param array|null $allowedCallers + * @param array|null $outputSchema */ private function __construct( public readonly string $name, @@ -32,6 +34,8 @@ private function __construct( public readonly bool $strict, public readonly string $type, public readonly ?string $description = null, + public readonly ?array $allowedCallers = null, + public readonly ?array $outputSchema = null, ) {} /** @@ -45,6 +49,8 @@ public static function from(array $attributes): self strict: $attributes['strict'], type: $attributes['type'], description: $attributes['description'] ?? null, + allowedCallers: $attributes['allowed_callers'] ?? null, + outputSchema: $attributes['output_schema'] ?? null, ); } @@ -53,12 +59,22 @@ public static function from(array $attributes): self */ public function toArray(): array { - return [ + $attributes = [ 'name' => $this->name, 'parameters' => $this->parameters, 'strict' => $this->strict, 'type' => $this->type, 'description' => $this->description, ]; + + if ($this->allowedCallers !== null) { + $attributes['allowed_callers'] = $this->allowedCallers; + } + + if ($this->outputSchema !== null) { + $attributes['output_schema'] = $this->outputSchema; + } + + return $attributes; } } diff --git a/src/Responses/Responses/Tool/NamespaceTools/CustomTool.php b/src/Responses/Responses/Tool/NamespaceTools/CustomTool.php index 824fc22b2..b6e645d3c 100644 --- a/src/Responses/Responses/Tool/NamespaceTools/CustomTool.php +++ b/src/Responses/Responses/Tool/NamespaceTools/CustomTool.php @@ -14,7 +14,7 @@ /** * @phpstan-import-type CustomToolInputTypes from CustomToolInputObjects * - * @phpstan-type CustomToolType array{name: string, type: 'custom', defer_loading?: bool, description?: string, format?: CustomToolInputTypes} + * @phpstan-type CustomToolType array{name: string, type: 'custom', defer_loading?: bool, description?: string, format?: CustomToolInputTypes, allowed_callers?: array|null} * * @implements ResponseContract */ @@ -29,6 +29,7 @@ final class CustomTool implements ResponseContract /** * @param 'custom' $type + * @param array|null $allowedCallers */ private function __construct( public readonly string $name, @@ -36,6 +37,7 @@ private function __construct( public readonly ?bool $deferLoading = null, public readonly ?string $description = null, public readonly TextInput|GrammarInput|null $format = null, + public readonly ?array $allowedCallers = null, ) {} /** @@ -51,6 +53,7 @@ public static function from(array $attributes): self format: isset($attributes['format']) ? CustomToolInputObjects::parse($attributes['format']) : null, + allowedCallers: $attributes['allowed_callers'] ?? null, ); } @@ -65,6 +68,7 @@ public function toArray(): array 'defer_loading' => $this->deferLoading, 'description' => $this->description, 'format' => $this->format?->toArray(), + 'allowed_callers' => $this->allowedCallers, ], fn (mixed $value): bool => $value !== null); } } diff --git a/src/Responses/Responses/Tool/NamespaceTools/FunctionTool.php b/src/Responses/Responses/Tool/NamespaceTools/FunctionTool.php index 623e8fe38..802ced5ce 100644 --- a/src/Responses/Responses/Tool/NamespaceTools/FunctionTool.php +++ b/src/Responses/Responses/Tool/NamespaceTools/FunctionTool.php @@ -9,7 +9,7 @@ use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @phpstan-type FunctionToolType array{name: string, parameters: array, strict: bool, type: 'function', description: ?string} + * @phpstan-type FunctionToolType array{name: string, parameters: array, strict: bool, type: 'function', description: ?string, allowed_callers?: array|null, output_schema?: array|null} * * @implements ResponseContract */ @@ -25,6 +25,8 @@ final class FunctionTool implements ResponseContract /** * @param array $parameters * @param 'function' $type + * @param array|null $allowedCallers + * @param array|null $outputSchema */ private function __construct( public readonly string $name, @@ -32,6 +34,8 @@ private function __construct( public readonly bool $strict, public readonly string $type, public readonly ?string $description = null, + public readonly ?array $allowedCallers = null, + public readonly ?array $outputSchema = null, ) {} /** @@ -45,6 +49,8 @@ public static function from(array $attributes): self strict: $attributes['strict'], type: $attributes['type'], description: $attributes['description'] ?? null, + allowedCallers: $attributes['allowed_callers'] ?? null, + outputSchema: $attributes['output_schema'] ?? null, ); } @@ -53,12 +59,22 @@ public static function from(array $attributes): self */ public function toArray(): array { - return [ + $attributes = [ 'name' => $this->name, 'parameters' => $this->parameters, 'strict' => $this->strict, 'type' => $this->type, 'description' => $this->description, ]; + + if ($this->allowedCallers !== null) { + $attributes['allowed_callers'] = $this->allowedCallers; + } + + if ($this->outputSchema !== null) { + $attributes['output_schema'] = $this->outputSchema; + } + + return $attributes; } } diff --git a/src/Responses/Responses/Tool/ProgrammaticToolCallingTool.php b/src/Responses/Responses/Tool/ProgrammaticToolCallingTool.php new file mode 100644 index 000000000..77a04413d --- /dev/null +++ b/src/Responses/Responses/Tool/ProgrammaticToolCallingTool.php @@ -0,0 +1,51 @@ + + */ +final class ProgrammaticToolCallingTool implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'programmatic_tool_calling' $type + */ + private function __construct( + public readonly string $type, + ) {} + + /** + * @param ProgrammaticToolCallingToolType $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/RemoteMcpTool.php b/src/Responses/Responses/Tool/RemoteMcpTool.php index 00d2a6fb2..7eb9b27a2 100644 --- a/src/Responses/Responses/Tool/RemoteMcpTool.php +++ b/src/Responses/Responses/Tool/RemoteMcpTool.php @@ -11,7 +11,7 @@ /** * @phpstan-import-type McpToolNamesFilterType from McpToolNamesFilter * - * @phpstan-type RemoteMcpToolType array{type: 'mcp', server_label: string, authorization: string|null, connector_id?: string|null, server_url: string|null, require_approval: 'never'|'always'|array<'never'|'always', McpToolNamesFilterType>|null, allowed_tools: array|McpToolNamesFilterType|null, headers: array|null, server_description?: string|null} + * @phpstan-type RemoteMcpToolType array{type: 'mcp', server_label: string, authorization: string|null, connector_id?: string|null, server_url: string|null, require_approval: 'never'|'always'|array<'never'|'always', McpToolNamesFilterType>|null, allowed_tools: array|McpToolNamesFilterType|null, headers: array|null, server_description?: string|null, allowed_callers?: array|null} * * @implements ResponseContract */ @@ -29,6 +29,7 @@ final class RemoteMcpTool implements ResponseContract * @param 'never'|'always'|array<'never'|'always', McpToolNamesFilter>|null $requireApproval * @param array|McpToolNamesFilter|null $allowedTools * @param array|null $headers + * @param array|null $allowedCallers */ private function __construct( public readonly string $type, @@ -40,6 +41,7 @@ private function __construct( public readonly ?string $connectorId = null, public readonly ?string $authorization = null, public readonly ?string $serverDescription = null, + public readonly ?array $allowedCallers = null, ) {} /** @@ -72,6 +74,7 @@ function (array $approvalAttributes): McpToolNamesFilter { connectorId: $attributes['connector_id'] ?? null, authorization: $attributes['authorization'] ?? null, serverDescription: $attributes['server_description'] ?? null, + allowedCallers: $attributes['allowed_callers'] ?? null, ); } @@ -92,7 +95,7 @@ public function toArray(): array $allowedTools = $allowedTools->toArray(); } - return [ + $result = [ 'type' => $this->type, 'server_label' => $this->serverLabel, 'server_url' => $this->serverUrl, @@ -103,5 +106,11 @@ public function toArray(): array 'authorization' => $this->authorization, 'server_description' => $this->serverDescription, ]; + + if ($this->allowedCallers !== null) { + $result['allowed_callers'] = $this->allowedCallers; + } + + return $result; } } diff --git a/src/Responses/Responses/ToolChoice/HostedToolChoice.php b/src/Responses/Responses/ToolChoice/HostedToolChoice.php index 56e2895e6..35e535f57 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'|'web_search_preview'|'computer_use_preview'} + * @phpstan-type HostedToolChoiceType array{type: 'file_search'|'web_search'|'web_search_preview'|'computer_use_preview'|'programmatic_tool_calling'} * * @implements ResponseContract */ @@ -23,7 +23,7 @@ final class HostedToolChoice implements ResponseContract use Fakeable; /** - * @param 'file_search'|'web_search'|'web_search_preview'|'computer_use_preview' $type + * @param 'file_search'|'web_search'|'web_search_preview'|'computer_use_preview'|'programmatic_tool_calling' $type */ private function __construct( public readonly string $type, diff --git a/tests/Fixtures/Responses.php b/tests/Fixtures/Responses.php index 4da44a0f7..a220654e9 100644 --- a/tests/Fixtures/Responses.php +++ b/tests/Fixtures/Responses.php @@ -229,6 +229,10 @@ function listInputItemsResource(): array outputCodeInterpreterToolCall(), outputLocalShellCall(), outputCustomToolCall(), + outputProgram(), + outputProgramOutput(), + functionToolCallOutputItem(), + customToolCallOutputItem(), ], 'first_id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', 'last_id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', @@ -290,6 +294,28 @@ function customToolCallOutputItem(): array 'call_id' => 'call_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', 'id' => 'cto_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', 'output' => 'custom-output', + 'caller' => [ + 'type' => 'program', + 'caller_id' => 'call_prog_123', + ], + ]; +} + +/** + * @return array + */ +function functionToolCallOutputItem(): array +{ + return [ + 'call_id' => 'call_inventory_123', + 'id' => 'fco_123', + 'output' => '{"sku":"sku_123","available_units":42}', + 'type' => 'function_call_output', + 'status' => 'completed', + 'caller' => [ + 'type' => 'program', + 'caller_id' => 'call_prog_123', + ], ]; } @@ -723,6 +749,56 @@ function outputCustomToolCall(): array 'input' => 'ls -l', 'name' => 'my_custom_tool', 'id' => 'ct_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'caller' => [ + 'type' => 'program', + 'caller_id' => 'call_prog_123', + ], + ]; +} + +/** + * @return array + */ +function outputProgram(): array +{ + return [ + 'type' => 'program', + 'id' => 'prog_123', + 'call_id' => 'call_prog_123', + 'code' => "const [stock, demand] = await Promise.all([tools.get_inventory({ sku: 'sku_123' }), tools.get_demand({ sku: 'sku_123' })]); text(JSON.stringify({ sku: stock.sku, available_units: stock.available_units, requested_units: demand.requested_units, shortage_units: Math.max(demand.requested_units - stock.available_units, 0) }));", + 'fingerprint' => 'opaque_replay_state', + ]; +} + +/** + * @return array + */ +function outputFunctionToolCallFromProgram(): array +{ + return [ + 'arguments' => '{"sku":"sku_123"}', + 'call_id' => 'call_inventory_123', + 'name' => 'get_inventory', + 'type' => 'function_call', + 'id' => 'fc_123', + 'caller' => [ + 'type' => 'program', + 'caller_id' => 'call_prog_123', + ], + ]; +} + +/** + * @return array + */ +function outputProgramOutput(): array +{ + return [ + 'type' => 'program_output', + 'id' => 'prog_out_123', + 'call_id' => 'call_prog_123', + 'result' => '{"sku":"sku_123","available_units":42,"requested_units":31,"shortage_units":0}', + 'status' => 'completed', ]; } @@ -797,6 +873,7 @@ function toolRemoteMcp(): array 'connector_id' => null, 'authorization' => null, 'server_description' => null, + 'allowed_callers' => ['direct', 'programmatic'], ]; } @@ -919,6 +996,47 @@ function toolCustom(): array ]; } +/** + * @return array + */ +function toolFunctionProgrammatic(): array +{ + return [ + 'name' => 'get_inventory', + 'parameters' => [ + 'type' => 'object', + 'properties' => [ + 'sku' => ['type' => 'string'], + ], + 'required' => ['sku'], + 'additionalProperties' => false, + ], + 'strict' => true, + 'type' => 'function', + 'description' => 'Return the inventory for a SKU.', + 'allowed_callers' => ['programmatic'], + 'output_schema' => [ + 'type' => 'object', + 'properties' => [ + 'sku' => ['type' => 'string'], + 'available_units' => ['type' => 'number'], + ], + 'required' => ['sku', 'available_units'], + 'additionalProperties' => false, + ], + ]; +} + +/** + * @return array + */ +function toolProgrammaticToolCalling(): array +{ + return [ + 'type' => 'programmatic_tool_calling', + ]; +} + /** * @return array */ diff --git a/tests/Responses/Conversations/ConversationItem.php b/tests/Responses/Conversations/ConversationItem.php index bacf64d66..428b7d5ea 100644 --- a/tests/Responses/Conversations/ConversationItem.php +++ b/tests/Responses/Conversations/ConversationItem.php @@ -2,6 +2,8 @@ use OpenAI\Responses\Conversations\ConversationItem; use OpenAI\Responses\Conversations\Objects\Message; +use OpenAI\Responses\Responses\Output\OutputProgram; +use OpenAI\Responses\Responses\Output\OutputProgramOutput; test('from', function () { $response = ConversationItem::from(conversationItemResource()); @@ -25,3 +27,21 @@ expect($response->toArray()) ->toBe(conversationItemResource()); }); + +test('from program item', function () { + $response = ConversationItem::from(outputProgram()); + + expect($response) + ->item->toBeInstanceOf(OutputProgram::class) + ->item->callId->toBe('call_prog_123') + ->toArray()->toBe(outputProgram()); +}); + +test('from program output item', function () { + $response = ConversationItem::from(outputProgramOutput()); + + expect($response) + ->item->toBeInstanceOf(OutputProgramOutput::class) + ->item->callId->toBe('call_prog_123') + ->toArray()->toBe(outputProgramOutput()); +}); diff --git a/tests/Responses/Conversations/ConversationItemList.php b/tests/Responses/Conversations/ConversationItemList.php index 38124aaa9..cba7daee7 100644 --- a/tests/Responses/Conversations/ConversationItemList.php +++ b/tests/Responses/Conversations/ConversationItemList.php @@ -3,6 +3,8 @@ use OpenAI\Responses\Conversations\ConversationItem; use OpenAI\Responses\Conversations\ConversationItemList; use OpenAI\Responses\Meta\MetaInformation; +use OpenAI\Responses\Responses\Output\OutputProgram; +use OpenAI\Responses\Responses\Output\OutputProgramOutput; test('from', function () { $response = ConversationItemList::from(conversationItemListResource(), meta()); @@ -33,3 +35,16 @@ expect($response->toArray()) ->toBe(conversationItemListResource()); }); + +test('from program items', function () { + $attributes = conversationItemListResource(); + $attributes['data'] = [outputProgram(), outputProgramOutput()]; + + $response = ConversationItemList::from($attributes, meta()); + + expect($response->data) + ->toHaveCount(2); + expect($response->data[0]->item)->toBeInstanceOf(OutputProgram::class); + expect($response->data[1]->item)->toBeInstanceOf(OutputProgramOutput::class); + expect($response->toArray())->toBe($attributes); +}); diff --git a/tests/Responses/Responses/CreateResponse.php b/tests/Responses/Responses/CreateResponse.php index 37300b000..d42830f0b 100644 --- a/tests/Responses/Responses/CreateResponse.php +++ b/tests/Responses/Responses/CreateResponse.php @@ -5,6 +5,11 @@ use OpenAI\Responses\Responses\CreateResponseFormat; use OpenAI\Responses\Responses\CreateResponseReasoning; use OpenAI\Responses\Responses\CreateResponseUsage; +use OpenAI\Responses\Responses\Output\OutputFunctionToolCall; +use OpenAI\Responses\Responses\Output\OutputProgram; +use OpenAI\Responses\Responses\Output\OutputProgramOutput; +use OpenAI\Responses\Responses\Tool\ProgrammaticToolCallingTool; +use OpenAI\Responses\Responses\ToolChoice\HostedToolChoice; use OpenAI\Testing\Enums\OverrideStrategy; test('from', function () { @@ -183,3 +188,37 @@ ->toBeArray() ->text->toBeNull(); }); + +test('programmatic tool calling output', function () { + $payload = createResponseResource(); + $payload['output'] = [ + outputProgram(), + outputFunctionToolCallFromProgram(), + outputProgramOutput(), + ]; + $payload['tools'] = [ + toolFunctionProgrammatic(), + toolProgrammaticToolCalling(), + ]; + $payload['tool_choice'] = toolProgrammaticToolCalling(); + + $response = CreateResponse::from($payload, meta()); + + expect($response) + ->output->{0}->toBeInstanceOf(OutputProgram::class) + ->output->{1}->toBeInstanceOf(OutputFunctionToolCall::class) + ->output->{1}->caller->callerId->toBe('call_prog_123') + ->output->{2}->toBeInstanceOf(OutputProgramOutput::class) + ->tools->{1}->toBeInstanceOf(ProgrammaticToolCallingTool::class) + ->toolChoice->toBeInstanceOf(HostedToolChoice::class) + ->toolChoice->type->toBe('programmatic_tool_calling'); + + expect($response->toArray()['output']) + ->toBe($payload['output']); + + expect($response->toArray()['tools']) + ->toBe($payload['tools']); + + expect($response->toArray()['tool_choice']) + ->toBe($payload['tool_choice']); +}); diff --git a/tests/Responses/Responses/CreateStreamedResponse.php b/tests/Responses/Responses/CreateStreamedResponse.php index 907243fbd..ede404191 100644 --- a/tests/Responses/Responses/CreateStreamedResponse.php +++ b/tests/Responses/Responses/CreateStreamedResponse.php @@ -3,6 +3,9 @@ use OpenAI\Responses\Responses\CreateResponse; use OpenAI\Responses\Responses\CreateStreamedResponse; use OpenAI\Responses\Responses\Output\OutputCompaction; +use OpenAI\Responses\Responses\Output\OutputFunctionToolCall; +use OpenAI\Responses\Responses\Output\OutputProgram; +use OpenAI\Responses\Responses\Output\OutputProgramOutput; use OpenAI\Responses\Responses\Streaming\OutputItem; use OpenAI\Responses\Responses\Streaming\RateLimits; use OpenAI\Responses\Responses\Streaming\ReasoningTextDelta; @@ -85,3 +88,49 @@ ->response->item->encryptedContent->toBe('encrypted_string_value') ->response->item->createdBy->toBe('user_123'); }); + +test('output item added event with program item', function () { + $response = CreateStreamedResponse::from([ + 'type' => 'response.output_item.added', + 'output_index' => 0, + 'sequence_number' => 1, + 'item' => outputProgram(), + '__meta' => meta(), + ]); + + expect($response) + ->event->toBe('response.output_item.added') + ->response->toBeInstanceOf(OutputItem::class) + ->response->item->toBeInstanceOf(OutputProgram::class); +}); + +test('output item done event with program function call', function () { + $response = CreateStreamedResponse::from([ + 'type' => 'response.output_item.done', + 'output_index' => 1, + 'sequence_number' => 2, + 'item' => outputFunctionToolCallFromProgram(), + '__meta' => meta(), + ]); + + expect($response) + ->event->toBe('response.output_item.done') + ->response->toBeInstanceOf(OutputItem::class) + ->response->item->toBeInstanceOf(OutputFunctionToolCall::class) + ->response->item->caller->callerId->toBe('call_prog_123'); +}); + +test('output item done event with program output item', function () { + $response = CreateStreamedResponse::from([ + 'type' => 'response.output_item.done', + 'output_index' => 2, + 'sequence_number' => 3, + 'item' => outputProgramOutput(), + '__meta' => meta(), + ]); + + expect($response) + ->event->toBe('response.output_item.done') + ->response->toBeInstanceOf(OutputItem::class) + ->response->item->toBeInstanceOf(OutputProgramOutput::class); +}); diff --git a/tests/Responses/Responses/Input/CustomToolCallOutput.php b/tests/Responses/Responses/Input/CustomToolCallOutput.php index 4dacf93e0..fb837aeed 100644 --- a/tests/Responses/Responses/Input/CustomToolCallOutput.php +++ b/tests/Responses/Responses/Input/CustomToolCallOutput.php @@ -10,7 +10,8 @@ ->type->toBe('custom_tool_call_output') ->id->toBe('cto_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c') ->callId->toBe('call_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c') - ->output->toBe('custom-output'); + ->output->toBe('custom-output') + ->caller->callerId->toBe('call_prog_123'); }); it('is array accessible', function () { diff --git a/tests/Responses/Responses/Input/FunctionToolCallOutput.php b/tests/Responses/Responses/Input/FunctionToolCallOutput.php new file mode 100644 index 000000000..c9914fb99 --- /dev/null +++ b/tests/Responses/Responses/Input/FunctionToolCallOutput.php @@ -0,0 +1,19 @@ +toBeInstanceOf(FunctionToolCallOutput::class) + ->type->toBe('function_call_output') + ->callId->toBe('call_inventory_123') + ->caller->callerId->toBe('call_prog_123'); +}); + +it('to array', function () { + $response = FunctionToolCallOutput::from(functionToolCallOutputItem()); + + expect($response->toArray())->toBe(functionToolCallOutputItem()); +}); diff --git a/tests/Responses/Responses/ListInputItems.php b/tests/Responses/Responses/ListInputItems.php index 0b6f3c2f8..4cca2154a 100644 --- a/tests/Responses/Responses/ListInputItems.php +++ b/tests/Responses/Responses/ListInputItems.php @@ -1,7 +1,11 @@ toBeInstanceOf(ListInputItems::class) ->object->toBe('list') ->data->toBeArray() - ->data->toHaveCount(13) + ->data->toHaveCount(17) ->firstId->toBe('msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c') ->lastId->toBe('msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c') ->hasMore->toBeFalse() ->meta()->toBeInstanceOf(MetaInformation::class); + + expect($result->data) + ->{13}->toBeInstanceOf(OutputProgram::class) + ->{14}->toBeInstanceOf(OutputProgramOutput::class) + ->{15}->toBeInstanceOf(FunctionToolCallOutput::class) + ->{15}->caller->callerId->toBe('call_prog_123') + ->{16}->toBeInstanceOf(CustomToolCallOutput::class) + ->{16}->caller->callerId->toBe('call_prog_123'); }); test('as array accessible', function () { diff --git a/tests/Responses/Responses/Output/OutputCustomToolCall.php b/tests/Responses/Responses/Output/OutputCustomToolCall.php index 908a45dd9..7f0d785a2 100644 --- a/tests/Responses/Responses/Output/OutputCustomToolCall.php +++ b/tests/Responses/Responses/Output/OutputCustomToolCall.php @@ -11,5 +11,8 @@ ->callId->toBe('call_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c') ->input->toBe('ls -l') ->name->toBe('my_custom_tool') - ->id->toBe('ct_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c'); + ->id->toBe('ct_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c') + ->caller->callerId->toBe('call_prog_123'); + + expect($response->toArray())->toBe(outputCustomToolCall()); }); diff --git a/tests/Responses/Responses/Output/OutputFunctionToolCall.php b/tests/Responses/Responses/Output/OutputFunctionToolCall.php new file mode 100644 index 000000000..33ab21373 --- /dev/null +++ b/tests/Responses/Responses/Output/OutputFunctionToolCall.php @@ -0,0 +1,23 @@ +toBeInstanceOf(OutputFunctionToolCall::class) + ->type->toBe('function_call') + ->status->toBeNull() + ->caller->toBeInstanceOf(OutputFunctionToolCallCaller::class) + ->caller->type->toBe('program') + ->caller->callerId->toBe('call_prog_123'); +}); + +test('to array with program caller', function () { + $response = OutputFunctionToolCall::from(outputFunctionToolCallFromProgram()); + + expect($response->toArray()) + ->toBe(outputFunctionToolCallFromProgram()); +}); diff --git a/tests/Responses/Responses/Output/OutputProgram.php b/tests/Responses/Responses/Output/OutputProgram.php new file mode 100644 index 000000000..24523835e --- /dev/null +++ b/tests/Responses/Responses/Output/OutputProgram.php @@ -0,0 +1,28 @@ +toBeInstanceOf(OutputProgram::class) + ->type->toBe('program') + ->id->toBe('prog_123') + ->callId->toBe('call_prog_123') + ->code->toBe(outputProgram()['code']) + ->fingerprint->toBe('opaque_replay_state'); +}); + +test('as array accessible', function () { + $response = OutputProgram::from(outputProgram()); + + expect($response['call_id'])->toBe('call_prog_123'); +}); + +test('to array', function () { + $response = OutputProgram::from(outputProgram()); + + expect($response->toArray()) + ->toBe(outputProgram()); +}); diff --git a/tests/Responses/Responses/Output/OutputProgramOutput.php b/tests/Responses/Responses/Output/OutputProgramOutput.php new file mode 100644 index 000000000..72cc49efe --- /dev/null +++ b/tests/Responses/Responses/Output/OutputProgramOutput.php @@ -0,0 +1,28 @@ +toBeInstanceOf(OutputProgramOutput::class) + ->type->toBe('program_output') + ->id->toBe('prog_out_123') + ->callId->toBe('call_prog_123') + ->result->toBe(outputProgramOutput()['result']) + ->status->toBe('completed'); +}); + +test('as array accessible', function () { + $response = OutputProgramOutput::from(outputProgramOutput()); + + expect($response['result'])->toBe(outputProgramOutput()['result']); +}); + +test('to array', function () { + $response = OutputProgramOutput::from(outputProgramOutput()); + + expect($response->toArray()) + ->toBe(outputProgramOutput()); +}); diff --git a/tests/Responses/Responses/Tool/CodeInterpreterTool.php b/tests/Responses/Responses/Tool/CodeInterpreterTool.php new file mode 100644 index 000000000..5addea8b7 --- /dev/null +++ b/tests/Responses/Responses/Tool/CodeInterpreterTool.php @@ -0,0 +1,18 @@ + 'cntr_123', + 'type' => 'code_interpreter', + 'allowed_callers' => ['programmatic'], + ]; + + $response = CodeInterpreterTool::from($attributes); + + expect($response) + ->toBeInstanceOf(CodeInterpreterTool::class) + ->allowedCallers->toBe(['programmatic']) + ->toArray()->toBe($attributes); +}); diff --git a/tests/Responses/Responses/Tool/CustomTool.php b/tests/Responses/Responses/Tool/CustomTool.php new file mode 100644 index 000000000..8171ac7e8 --- /dev/null +++ b/tests/Responses/Responses/Tool/CustomTool.php @@ -0,0 +1,21 @@ +toBeInstanceOf(CustomTool::class) + ->allowedCallers->toBe(['programmatic']); +}); + +it('to array', function () { + $attributes = toolCustom(); + $attributes['allowed_callers'] = ['programmatic']; + $response = CustomTool::from($attributes); + + expect($response->toArray())->toBe($attributes); +}); diff --git a/tests/Responses/Responses/Tool/CustomToolTest.php b/tests/Responses/Responses/Tool/CustomToolTest.php index 801d25a39..ffe4fa3f0 100644 --- a/tests/Responses/Responses/Tool/CustomToolTest.php +++ b/tests/Responses/Responses/Tool/CustomToolTest.php @@ -42,3 +42,18 @@ expect($response->toArray()) ->toBe($data); }); + +test('namespace custom tool with programmatic callers', function () { + $data = toolNamespace(); + $customTool = toolCustom(); + $customTool['allowed_callers'] = ['programmatic']; + $data['tools'] = [$customTool]; + + $response = NamespaceTool::from($data); + + expect($response->tools[0]) + ->toBeInstanceOf(NamespaceCustomTool::class) + ->allowedCallers->toBe(['programmatic']); + + expect($response->toArray())->toBe($data); +}); diff --git a/tests/Responses/Responses/Tool/FunctionTool.php b/tests/Responses/Responses/Tool/FunctionTool.php new file mode 100644 index 000000000..76d10735e --- /dev/null +++ b/tests/Responses/Responses/Tool/FunctionTool.php @@ -0,0 +1,19 @@ +toBeInstanceOf(FunctionTool::class) + ->allowedCallers->toBe(['programmatic']) + ->outputSchema->toBe(toolFunctionProgrammatic()['output_schema']); +}); + +test('to array with programmatic tool calling fields', function () { + $response = FunctionTool::from(toolFunctionProgrammatic()); + + expect($response->toArray()) + ->toBe(toolFunctionProgrammatic()); +}); diff --git a/tests/Responses/Responses/Tool/NamespaceTool.php b/tests/Responses/Responses/Tool/NamespaceTool.php index 58019c381..9b1f94ca5 100644 --- a/tests/Responses/Responses/Tool/NamespaceTool.php +++ b/tests/Responses/Responses/Tool/NamespaceTool.php @@ -1,6 +1,7 @@ toBeArray() ->toBe(toolNamespace()); }); + +test('function tool with programmatic configuration', function () { + $data = toolNamespace(); + $data['tools'] = [[ + 'name' => 'get_inventory', + 'parameters' => [ + 'type' => 'object', + 'properties' => [ + 'sku' => ['type' => 'string'], + ], + 'required' => ['sku'], + 'additionalProperties' => false, + ], + 'strict' => true, + 'type' => 'function', + 'description' => 'Return inventory for a SKU.', + 'allowed_callers' => ['programmatic'], + 'output_schema' => [ + 'type' => 'object', + 'properties' => [ + 'available_units' => ['type' => 'number'], + ], + 'required' => ['available_units'], + 'additionalProperties' => false, + ], + ]]; + + $response = NamespaceTool::from($data); + + expect($response->tools[0]) + ->toBeInstanceOf(FunctionTool::class) + ->allowedCallers->toBe(['programmatic']) + ->outputSchema->toBe($data['tools'][0]['output_schema']); + + expect($response->toArray())->toBe($data); +}); diff --git a/tests/Responses/Responses/Tool/ProgrammaticToolCallingTool.php b/tests/Responses/Responses/Tool/ProgrammaticToolCallingTool.php new file mode 100644 index 000000000..c3326e6e0 --- /dev/null +++ b/tests/Responses/Responses/Tool/ProgrammaticToolCallingTool.php @@ -0,0 +1,24 @@ +toBeInstanceOf(ProgrammaticToolCallingTool::class) + ->type->toBe('programmatic_tool_calling'); +}); + +test('as array accessible', function () { + $response = ProgrammaticToolCallingTool::from(toolProgrammaticToolCalling()); + + expect($response['type'])->toBe('programmatic_tool_calling'); +}); + +test('to array', function () { + $response = ProgrammaticToolCallingTool::from(toolProgrammaticToolCalling()); + + expect($response->toArray()) + ->toBe(toolProgrammaticToolCalling()); +}); diff --git a/tests/Responses/Responses/Tool/RemoteMcpTool.php b/tests/Responses/Responses/Tool/RemoteMcpTool.php index dfda7ab70..b0a6891f0 100644 --- a/tests/Responses/Responses/Tool/RemoteMcpTool.php +++ b/tests/Responses/Responses/Tool/RemoteMcpTool.php @@ -10,7 +10,8 @@ ->toBeInstanceOf(RemoteMcpTool::class) ->type->toBe('mcp') ->serverLabel->toBe('My test MCP server') - ->serverUrl->toBe('https://server.example.com/mcp'); + ->serverUrl->toBe('https://server.example.com/mcp') + ->allowedCallers->toBe(['direct', 'programmatic']); }); test('from results', function () { From 75e7b0f9a5848548296f163bba91ceb2094079a9 Mon Sep 17 00:00:00 2001 From: Alexander Morozov Date: Mon, 20 Jul 2026 15:19:16 +0400 Subject: [PATCH 3/3] Add fixtures for programmatic DTOs --- .../Output/OutputFunctionToolCallCallerFixture.php | 11 +++++++++++ .../Responses/Output/OutputProgramFixture.php | 14 ++++++++++++++ .../Output/OutputProgramOutputFixture.php | 14 ++++++++++++++ .../Tool/ProgrammaticToolCallingToolFixture.php | 10 ++++++++++ 4 files changed, 49 insertions(+) create mode 100644 src/Testing/Responses/Fixtures/Responses/Output/OutputFunctionToolCallCallerFixture.php create mode 100644 src/Testing/Responses/Fixtures/Responses/Output/OutputProgramFixture.php create mode 100644 src/Testing/Responses/Fixtures/Responses/Output/OutputProgramOutputFixture.php create mode 100644 src/Testing/Responses/Fixtures/Responses/Tool/ProgrammaticToolCallingToolFixture.php diff --git a/src/Testing/Responses/Fixtures/Responses/Output/OutputFunctionToolCallCallerFixture.php b/src/Testing/Responses/Fixtures/Responses/Output/OutputFunctionToolCallCallerFixture.php new file mode 100644 index 000000000..af708b7e5 --- /dev/null +++ b/src/Testing/Responses/Fixtures/Responses/Output/OutputFunctionToolCallCallerFixture.php @@ -0,0 +1,11 @@ + 'program', + 'caller_id' => 'call_prog_123', + ]; +} diff --git a/src/Testing/Responses/Fixtures/Responses/Output/OutputProgramFixture.php b/src/Testing/Responses/Fixtures/Responses/Output/OutputProgramFixture.php new file mode 100644 index 000000000..e4529ac27 --- /dev/null +++ b/src/Testing/Responses/Fixtures/Responses/Output/OutputProgramFixture.php @@ -0,0 +1,14 @@ + 'program', + 'id' => 'prog_123', + 'call_id' => 'call_prog_123', + 'code' => "const stock = await tools.get_inventory({ sku: 'sku_123' }); text(JSON.stringify(stock));", + 'fingerprint' => 'opaque_replay_state', + ]; +} diff --git a/src/Testing/Responses/Fixtures/Responses/Output/OutputProgramOutputFixture.php b/src/Testing/Responses/Fixtures/Responses/Output/OutputProgramOutputFixture.php new file mode 100644 index 000000000..4e41bf028 --- /dev/null +++ b/src/Testing/Responses/Fixtures/Responses/Output/OutputProgramOutputFixture.php @@ -0,0 +1,14 @@ + 'program_output', + 'id' => 'prog_out_123', + 'call_id' => 'call_prog_123', + 'result' => '{"sku":"sku_123","available_units":42}', + 'status' => 'completed', + ]; +} diff --git a/src/Testing/Responses/Fixtures/Responses/Tool/ProgrammaticToolCallingToolFixture.php b/src/Testing/Responses/Fixtures/Responses/Tool/ProgrammaticToolCallingToolFixture.php new file mode 100644 index 000000000..399e5a3da --- /dev/null +++ b/src/Testing/Responses/Fixtures/Responses/Tool/ProgrammaticToolCallingToolFixture.php @@ -0,0 +1,10 @@ + 'programmatic_tool_calling', + ]; +}