From 7043e51d4f7ba62070eb80ce74bebd644f170f9b Mon Sep 17 00:00:00 2001 From: Michael Sitarzewski Date: Sat, 21 Jun 2025 10:44:48 -0500 Subject: [PATCH 1/3] - `CreateResponse.php` - Added code interpreter to response types - `CreateStreamedResponse.php` - Added streaming support for code interpreter - `OutputCodeInterpreterToolCall.php` - New file for code interpreter output handling - `OutputMessageContentOutputTextAnnotationsContainerFile.php` - New file for container file annotations - `OutputMessageContentOutputText.php` - Updated to support container file annotations - `CodeInterpreterCall.php` - New streaming event handler - `OutputTextAnnotationAdded.php` - New streaming event for text annotations - `CodeInterpreterCodeDone.php` - New streaming event for code completion - `CodeInterpreterCodeDelta.php` - New streaming event for code deltas - `OutputItem.php` - Updated to handle code interpreter outputs - `CodeInterpreterTool.php` - New tool type definition --- src/Responses/Responses/CreateResponse.php | 22 +++--- .../Responses/CreateStreamedResponse.php | 11 ++- .../Output/OutputCodeInterpreterToolCall.php | 57 +++++++++++++++ .../Output/OutputMessageContentOutputText.php | 26 +++++-- ...tentOutputTextAnnotationsContainerFile.php | 71 +++++++++++++++++++ .../Streaming/CodeInterpreterCall.php | 57 +++++++++++++++ .../Streaming/CodeInterpreterCodeDelta.php | 60 ++++++++++++++++ .../Streaming/CodeInterpreterCodeDone.php | 60 ++++++++++++++++ .../Responses/Streaming/OutputItem.php | 8 ++- .../Streaming/OutputTextAnnotationAdded.php | 24 +++++-- .../Responses/Tool/CodeInterpreterTool.php | 60 ++++++++++++++++ 11 files changed, 430 insertions(+), 26 deletions(-) create mode 100644 src/Responses/Responses/Output/OutputCodeInterpreterToolCall.php create mode 100644 src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsContainerFile.php create mode 100644 src/Responses/Responses/Streaming/CodeInterpreterCall.php create mode 100644 src/Responses/Responses/Streaming/CodeInterpreterCodeDelta.php create mode 100644 src/Responses/Responses/Streaming/CodeInterpreterCodeDone.php create mode 100644 src/Responses/Responses/Tool/CodeInterpreterTool.php diff --git a/src/Responses/Responses/CreateResponse.php b/src/Responses/Responses/CreateResponse.php index f2c31a4f..9988f806 100644 --- a/src/Responses/Responses/CreateResponse.php +++ b/src/Responses/Responses/CreateResponse.php @@ -9,6 +9,7 @@ use OpenAI\Responses\Concerns\ArrayAccessible; use OpenAI\Responses\Concerns\HasMetaInformation; use OpenAI\Responses\Meta\MetaInformation; +use OpenAI\Responses\Responses\Output\OutputCodeInterpreterToolCall; use OpenAI\Responses\Responses\Output\OutputComputerToolCall; use OpenAI\Responses\Responses\Output\OutputFileSearchToolCall; use OpenAI\Responses\Responses\Output\OutputFunctionToolCall; @@ -24,6 +25,7 @@ use OpenAI\Responses\Responses\Tool\FileSearchTool; use OpenAI\Responses\Responses\Tool\FunctionTool; use OpenAI\Responses\Responses\Tool\ImageGenerationTool; +use OpenAI\Responses\Responses\Tool\CodeInterpreterTool; use OpenAI\Responses\Responses\Tool\RemoteMcpTool; use OpenAI\Responses\Responses\Tool\WebSearchTool; use OpenAI\Responses\Responses\ToolChoice\FunctionToolChoice; @@ -32,6 +34,7 @@ /** * @phpstan-import-type ResponseFormatType from CreateResponseFormat + * @phpstan-import-type OutputCodeInterpreterToolCallType from OutputCodeInterpreterToolCall * @phpstan-import-type OutputComputerToolCallType from OutputComputerToolCall * @phpstan-import-type OutputFileSearchToolCallType from OutputFileSearchToolCall * @phpstan-import-type OutputFunctionToolCallType from OutputFunctionToolCall @@ -42,6 +45,7 @@ * @phpstan-import-type OutputMcpApprovalRequestType from OutputMcpApprovalRequest * @phpstan-import-type OutputMcpCallType from OutputMcpCall * @phpstan-import-type OutputImageGenerationToolCallType from OutputImageGenerationToolCall + * @phpstan-import-type CodeInterpreterToolType from CodeInterpreterTool * @phpstan-import-type ComputerUseToolType from ComputerUseTool * @phpstan-import-type FileSearchToolType from FileSearchTool * @phpstan-import-type ImageGenerationToolType from ImageGenerationTool @@ -56,8 +60,8 @@ * @phpstan-import-type ReasoningType from CreateResponseReasoning * * @phpstan-type ToolChoiceType 'none'|'auto'|'required'|FunctionToolChoiceType|HostedToolChoiceType - * @phpstan-type ToolsType array - * @phpstan-type OutputType array + * @phpstan-type ToolsType array + * @phpstan-type OutputType array * @phpstan-type CreateResponseType array{id: string, object: 'response', created_at: int, status: 'completed'|'failed'|'in_progress'|'incomplete', error: ErrorType|null, incomplete_details: IncompleteDetailsType|null, instructions: string|null, max_output_tokens: int|null, model: string, output: OutputType, output_text: string|null, parallel_tool_calls: bool, previous_response_id: string|null, reasoning: ReasoningType|null, store: bool, temperature: float|null, text: ResponseFormatType, tool_choice: ToolChoiceType, tools: ToolsType, top_p: float|null, truncation: 'auto'|'disabled'|null, usage: UsageType|null, user: string|null, metadata: array|null} * * @implements ResponseContract @@ -75,8 +79,8 @@ final class CreateResponse implements ResponseContract, ResponseHasMetaInformati /** * @param 'response' $object * @param 'completed'|'failed'|'in_progress'|'incomplete' $status - * @param array $output - * @param array $tools + * @param array $output + * @param array $tools * @param 'auto'|'disabled'|null $truncation * @param array $metadata */ @@ -114,7 +118,7 @@ private function __construct( public static function from(array $attributes, MetaInformation $meta): self { $output = array_map( - fn (array $output): OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall => match ($output['type']) { + fn (array $output): OutputMessage|OutputCodeInterpreterToolCall|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall => match ($output['type']) { 'message' => OutputMessage::from($output), 'file_search_call' => OutputFileSearchToolCall::from($output), 'function_call' => OutputFunctionToolCall::from($output), @@ -125,6 +129,7 @@ public static function from(array $attributes, MetaInformation $meta): self 'mcp_approval_request' => OutputMcpApprovalRequest::from($output), 'mcp_call' => OutputMcpCall::from($output), 'image_generation_call' => OutputImageGenerationToolCall::from($output), + 'code_interpreter_call' => OutputCodeInterpreterToolCall::from($output), }, $attributes['output'], ); @@ -137,13 +142,14 @@ public static function from(array $attributes, MetaInformation $meta): self : $attributes['tool_choice']; $tools = array_map( - fn (array $tool): ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool => match ($tool['type']) { + fn (array $tool): ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool|CodeInterpreterTool => match ($tool['type']) { 'file_search' => FileSearchTool::from($tool), 'web_search_preview', 'web_search_preview_2025_03_11' => WebSearchTool::from($tool), 'function' => FunctionTool::from($tool), 'computer_use_preview' => ComputerUseTool::from($tool), 'image_generation' => ImageGenerationTool::from($tool), 'mcp' => RemoteMcpTool::from($tool), + 'code_interpreter' => CodeInterpreterTool::from($tool), }, $attributes['tools'], ); @@ -216,7 +222,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 $output): array => $output->toArray(), + fn (OutputMessage|OutputCodeInterpreterToolCall|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall $output): array => $output->toArray(), $this->output ), 'parallel_tool_calls' => $this->parallelToolCalls, @@ -229,7 +235,7 @@ public function toArray(): array ? $this->toolChoice : $this->toolChoice->toArray(), 'tools' => array_map( - fn (ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool $tool): array => $tool->toArray(), + fn (CodeInterpreterTool|ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool|RemoteMcpTool $tool): array => $tool->toArray(), $this->tools ), 'top_p' => $this->topP, diff --git a/src/Responses/Responses/CreateStreamedResponse.php b/src/Responses/Responses/CreateStreamedResponse.php index df720176..fdd8791b 100644 --- a/src/Responses/Responses/CreateStreamedResponse.php +++ b/src/Responses/Responses/CreateStreamedResponse.php @@ -7,6 +7,9 @@ use OpenAI\Contracts\ResponseContract; use OpenAI\Exceptions\UnknownEventException; use OpenAI\Responses\Concerns\ArrayAccessible; +use OpenAI\Responses\Responses\Streaming\CodeInterpreterCall; +use OpenAI\Responses\Responses\Streaming\CodeInterpreterCodeDelta; +use OpenAI\Responses\Responses\Streaming\CodeInterpreterCodeDone; use OpenAI\Responses\Responses\Streaming\ContentPart; use OpenAI\Responses\Responses\Streaming\Error; use OpenAI\Responses\Responses\Streaming\FileSearchCall; @@ -47,7 +50,7 @@ final class CreateStreamedResponse implements ResponseContract private function __construct( public readonly string $event, - public readonly CreateResponse|OutputItem|ContentPart|OutputTextDelta|OutputTextAnnotationAdded|OutputTextDone|RefusalDelta|RefusalDone|FunctionCallArgumentsDelta|FunctionCallArgumentsDone|FileSearchCall|WebSearchCall|ReasoningSummaryPart|ReasoningSummaryTextDelta|ReasoningSummaryTextDone|McpListTools|McpListToolsInProgress|McpCall|McpCallArgumentsDelta|McpCallArgumentsDone|ImageGenerationPart|ImageGenerationPartialImage|Error $response, + public readonly CreateResponse|OutputItem|ContentPart|OutputTextDelta|OutputTextAnnotationAdded|OutputTextDone|RefusalDelta|RefusalDone|FunctionCallArgumentsDelta|FunctionCallArgumentsDone|FileSearchCall|WebSearchCall|CodeInterpreterCall|CodeInterpreterCodeDelta|CodeInterpreterCodeDone|ReasoningSummaryPart|ReasoningSummaryTextDelta|ReasoningSummaryTextDone|McpListTools|McpListToolsInProgress|McpCall|McpCallArgumentsDelta|McpCallArgumentsDone|ImageGenerationPart|ImageGenerationPartialImage|Error $response, ) {} /** @@ -82,6 +85,12 @@ public static function from(array $attributes): self 'response.web_search_call.in_progress', 'response.web_search_call.searching', 'response.web_search_call.completed' => WebSearchCall::from($attributes, $meta), // @phpstan-ignore-line + 'response.code_interpreter_call.in_progress', + 'response.code_interpreter_call.running', + 'response.code_interpreter_call.interpreting', + 'response.code_interpreter_call.completed' => CodeInterpreterCall::from($attributes, $meta), // @phpstan-ignore-line + 'response.code_interpreter_call_code.delta' => CodeInterpreterCodeDelta::from($attributes, $meta), // @phpstan-ignore-line + 'response.code_interpreter_call_code.done' => CodeInterpreterCodeDone::from($attributes, $meta), // @phpstan-ignore-line 'response.reasoning_summary_part.added', 'response.reasoning_summary_part.done' => ReasoningSummaryPart::from($attributes, $meta), // @phpstan-ignore-line 'response.reasoning_summary_text.delta' => ReasoningSummaryTextDelta::from($attributes, $meta), // @phpstan-ignore-line diff --git a/src/Responses/Responses/Output/OutputCodeInterpreterToolCall.php b/src/Responses/Responses/Output/OutputCodeInterpreterToolCall.php new file mode 100644 index 00000000..f783b673 --- /dev/null +++ b/src/Responses/Responses/Output/OutputCodeInterpreterToolCall.php @@ -0,0 +1,57 @@ + + */ +final class OutputCodeInterpreterToolCall implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'code_interpreter_call' $type + */ + private function __construct( + public readonly string $id, + public readonly string $status, + public readonly string $type, + ) {} + + /** + * @param OutputCodeInterpreterToolCallType $attributes + */ + public static function from(array $attributes): self + { + return new self( + id: $attributes['id'], + status: $attributes['status'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'status' => $this->status, + 'type' => $this->type, + ]; + } +} \ No newline at end of file diff --git a/src/Responses/Responses/Output/OutputMessageContentOutputText.php b/src/Responses/Responses/Output/OutputMessageContentOutputText.php index 77eedbfe..21ae7915 100644 --- a/src/Responses/Responses/Output/OutputMessageContentOutputText.php +++ b/src/Responses/Responses/Output/OutputMessageContentOutputText.php @@ -6,17 +6,19 @@ use OpenAI\Contracts\ResponseContract; use OpenAI\Responses\Concerns\ArrayAccessible; +use OpenAI\Responses\Responses\Output\OutputMessageContentOutputTextAnnotationsContainerFile as AnnotationContainerFile; use OpenAI\Responses\Responses\Output\OutputMessageContentOutputTextAnnotationsFileCitation as AnnotationFileCitation; use OpenAI\Responses\Responses\Output\OutputMessageContentOutputTextAnnotationsFilePath as AnnotationFilePath; use OpenAI\Responses\Responses\Output\OutputMessageContentOutputTextAnnotationsUrlCitation as AnnotationUrlCitation; use OpenAI\Testing\Responses\Concerns\Fakeable; /** + * @phpstan-import-type ContainerFileType from OutputMessageContentOutputTextAnnotationsContainerFile * @phpstan-import-type FileCitationType from OutputMessageContentOutputTextAnnotationsFileCitation * @phpstan-import-type FilePathType from OutputMessageContentOutputTextAnnotationsFilePath * @phpstan-import-type UrlCitationType from OutputMessageContentOutputTextAnnotationsUrlCitation * - * @phpstan-type OutputTextType array{annotations: array, text: string, type: 'output_text'} + * @phpstan-type OutputTextType array{annotations: array, text: string, type: 'output_text'} * * @implements ResponseContract */ @@ -30,7 +32,7 @@ final class OutputMessageContentOutputText implements ResponseContract use Fakeable; /** - * @param array $annotations + * @param array $annotations * @param 'output_text' $type */ private function __construct( @@ -45,10 +47,20 @@ private function __construct( public static function from(array $attributes): self { $annotations = array_map( - fn (array $annotation): AnnotationFileCitation|AnnotationFilePath|AnnotationUrlCitation => match ($annotation['type']) { - 'file_citation' => AnnotationFileCitation::from($annotation), - 'file_path' => AnnotationFilePath::from($annotation), - 'url_citation' => AnnotationUrlCitation::from($annotation), + function (array $annotation): AnnotationContainerFile|AnnotationFileCitation|AnnotationFilePath|AnnotationUrlCitation { + $annotationType = $annotation['type'] ?? 'unknown'; + + // Handle container_file types with any suffix + if (str_starts_with($annotationType, 'container_file')) { + return AnnotationContainerFile::from($annotation); + } + + return match ($annotationType) { + 'file_citation' => AnnotationFileCitation::from($annotation), + 'file_path' => AnnotationFilePath::from($annotation), + 'url_citation' => AnnotationUrlCitation::from($annotation), + default => throw new \UnhandledMatchError("Unhandled annotation type: {$annotationType}") + }; }, $attributes['annotations'], ); @@ -67,7 +79,7 @@ public function toArray(): array { return [ 'annotations' => array_map( - fn (AnnotationFileCitation|AnnotationFilePath|AnnotationUrlCitation $annotation): array => $annotation->toArray(), + fn (AnnotationContainerFile|AnnotationFileCitation|AnnotationFilePath|AnnotationUrlCitation $annotation): array => $annotation->toArray(), $this->annotations, ), 'text' => $this->text, diff --git a/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsContainerFile.php b/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsContainerFile.php new file mode 100644 index 00000000..efe3d977 --- /dev/null +++ b/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsContainerFile.php @@ -0,0 +1,71 @@ + + */ +final class OutputMessageContentOutputTextAnnotationsContainerFile implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly string $fileId, + public readonly string $type, + public readonly ?string $text, + public readonly ?int $startIndex, + public readonly ?int $endIndex, + ) {} + + /** + * @param ContainerFileType $attributes + */ + public static function from(array $attributes): self + { + return new self( + fileId: $attributes['file_id'], + type: $attributes['type'], + text: $attributes['text'] ?? null, + startIndex: $attributes['start_index'] ?? null, + endIndex: $attributes['end_index'] ?? null, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + $result = [ + 'file_id' => $this->fileId, + 'type' => $this->type, + ]; + + if ($this->text !== null) { + $result['text'] = $this->text; + } + + if ($this->startIndex !== null) { + $result['start_index'] = $this->startIndex; + } + + if ($this->endIndex !== null) { + $result['end_index'] = $this->endIndex; + } + + return $result; + } +} \ No newline at end of file diff --git a/src/Responses/Responses/Streaming/CodeInterpreterCall.php b/src/Responses/Responses/Streaming/CodeInterpreterCall.php new file mode 100644 index 00000000..908b64e6 --- /dev/null +++ b/src/Responses/Responses/Streaming/CodeInterpreterCall.php @@ -0,0 +1,57 @@ + + */ +final class CodeInterpreterCall implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + private function __construct( + public readonly string $itemId, + public readonly int $outputIndex, + private readonly MetaInformation $meta, + ) {} + + /** + * @param CodeInterpreterCallType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + return new self( + itemId: $attributes['item_id'], + outputIndex: $attributes['output_index'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'item_id' => $this->itemId, + 'output_index' => $this->outputIndex, + ]; + } +} \ No newline at end of file diff --git a/src/Responses/Responses/Streaming/CodeInterpreterCodeDelta.php b/src/Responses/Responses/Streaming/CodeInterpreterCodeDelta.php new file mode 100644 index 00000000..543ff56e --- /dev/null +++ b/src/Responses/Responses/Streaming/CodeInterpreterCodeDelta.php @@ -0,0 +1,60 @@ + + */ +final class CodeInterpreterCodeDelta implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + private function __construct( + public readonly string $delta, + public readonly string $itemId, + public readonly int $outputIndex, + private readonly MetaInformation $meta, + ) {} + + /** + * @param CodeInterpreterCodeDeltaType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + return new self( + delta: $attributes['delta'], + itemId: $attributes['item_id'], + outputIndex: $attributes['output_index'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'delta' => $this->delta, + 'item_id' => $this->itemId, + 'output_index' => $this->outputIndex, + ]; + } +} \ No newline at end of file diff --git a/src/Responses/Responses/Streaming/CodeInterpreterCodeDone.php b/src/Responses/Responses/Streaming/CodeInterpreterCodeDone.php new file mode 100644 index 00000000..d42bafdc --- /dev/null +++ b/src/Responses/Responses/Streaming/CodeInterpreterCodeDone.php @@ -0,0 +1,60 @@ + + */ +final class CodeInterpreterCodeDone implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + private function __construct( + public readonly string $code, + public readonly string $itemId, + public readonly int $outputIndex, + private readonly MetaInformation $meta, + ) {} + + /** + * @param CodeInterpreterCodeDoneType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + return new self( + code: $attributes['code'], + itemId: $attributes['item_id'], + outputIndex: $attributes['output_index'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'code' => $this->code, + 'item_id' => $this->itemId, + 'output_index' => $this->outputIndex, + ]; + } +} \ No newline at end of file diff --git a/src/Responses/Responses/Streaming/OutputItem.php b/src/Responses/Responses/Streaming/OutputItem.php index a601a585..6c959d70 100644 --- a/src/Responses/Responses/Streaming/OutputItem.php +++ b/src/Responses/Responses/Streaming/OutputItem.php @@ -9,6 +9,7 @@ use OpenAI\Responses\Concerns\ArrayAccessible; use OpenAI\Responses\Concerns\HasMetaInformation; use OpenAI\Responses\Meta\MetaInformation; +use OpenAI\Responses\Responses\Output\OutputCodeInterpreterToolCall; use OpenAI\Responses\Responses\Output\OutputComputerToolCall; use OpenAI\Responses\Responses\Output\OutputFileSearchToolCall; use OpenAI\Responses\Responses\Output\OutputFunctionToolCall; @@ -22,6 +23,7 @@ use OpenAI\Testing\Responses\Concerns\Fakeable; /** + * @phpstan-import-type OutputCodeInterpreterToolCallType from OutputCodeInterpreterToolCall * @phpstan-import-type OutputComputerToolCallType from OutputComputerToolCall * @phpstan-import-type OutputFileSearchToolCallType from OutputFileSearchToolCall * @phpstan-import-type OutputFunctionToolCallType from OutputFunctionToolCall @@ -32,9 +34,8 @@ * @phpstan-import-type OutputMcpListToolsType from OutputMcpListTools * @phpstan-import-type OutputMcpApprovalRequestType from OutputMcpApprovalRequest * @phpstan-import-type OutputMcpCallType from OutputMcpCall - * @phpstan-import-type OutputImageGenerationToolCallType from OutputImageGenerationToolCall * - * @phpstan-type OutputItemType array{item: OutputComputerToolCallType|OutputFileSearchToolCallType|OutputFunctionToolCallType|OutputMessageType|OutputReasoningType|OutputWebSearchToolCallType|OutputMcpListToolsType|OutputMcpApprovalRequestType|OutputMcpCallType|OutputImageGenerationToolCallType, output_index: int} + * @phpstan-type OutputItemType array{item: OutputCodeInterpreterToolCallType|OutputComputerToolCallType|OutputFileSearchToolCallType|OutputFunctionToolCallType|OutputMessageType|OutputReasoningType|OutputWebSearchToolCallType|OutputMcpListToolsType|OutputMcpApprovalRequestType|OutputMcpCallType|OutputImageGenerationToolCallType, output_index: int} * * @implements ResponseContract */ @@ -50,7 +51,7 @@ final class OutputItem implements ResponseContract, ResponseHasMetaInformationCo private function __construct( public readonly int $outputIndex, - public readonly OutputMessage|OutputFileSearchToolCall|OutputFunctionToolCall|OutputWebSearchToolCall|OutputComputerToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall $item, + public readonly OutputMessage|OutputCodeInterpreterToolCall|OutputFileSearchToolCall|OutputFunctionToolCall|OutputWebSearchToolCall|OutputComputerToolCall|OutputReasoning|OutputMcpListTools|OutputMcpApprovalRequest|OutputMcpCall|OutputImageGenerationToolCall $item, private readonly MetaInformation $meta, ) {} @@ -70,6 +71,7 @@ public static function from(array $attributes, MetaInformation $meta): self 'mcp_list_tools' => OutputMcpListTools::from($attributes['item']), 'mcp_approval_request' => OutputMcpApprovalRequest::from($attributes['item']), 'mcp_call' => OutputMcpCall::from($attributes['item']), + 'code_interpreter_call' => OutputCodeInterpreterToolCall::from($attributes['item']), }; return new self( diff --git a/src/Responses/Responses/Streaming/OutputTextAnnotationAdded.php b/src/Responses/Responses/Streaming/OutputTextAnnotationAdded.php index 0f8aeed2..a80dd57d 100644 --- a/src/Responses/Responses/Streaming/OutputTextAnnotationAdded.php +++ b/src/Responses/Responses/Streaming/OutputTextAnnotationAdded.php @@ -9,17 +9,19 @@ use OpenAI\Responses\Concerns\ArrayAccessible; use OpenAI\Responses\Concerns\HasMetaInformation; use OpenAI\Responses\Meta\MetaInformation; +use OpenAI\Responses\Responses\Output\OutputMessageContentOutputTextAnnotationsContainerFile; use OpenAI\Responses\Responses\Output\OutputMessageContentOutputTextAnnotationsFileCitation; use OpenAI\Responses\Responses\Output\OutputMessageContentOutputTextAnnotationsFilePath; use OpenAI\Responses\Responses\Output\OutputMessageContentOutputTextAnnotationsUrlCitation; use OpenAI\Testing\Responses\Concerns\Fakeable; /** + * @phpstan-import-type ContainerFileType from OutputMessageContentOutputTextAnnotationsContainerFile * @phpstan-import-type FileCitationType from OutputMessageContentOutputTextAnnotationsFileCitation * @phpstan-import-type FilePathType from OutputMessageContentOutputTextAnnotationsFilePath * @phpstan-import-type UrlCitationType from OutputMessageContentOutputTextAnnotationsUrlCitation * - * @phpstan-type OutputTextAnnotationAddedType array{annotation: FileCitationType|FilePathType|UrlCitationType, annotation_index: int, content_index: int, item_id: string, output_index: int} + * @phpstan-type OutputTextAnnotationAddedType array{annotation: ContainerFileType|FileCitationType|FilePathType|UrlCitationType, annotation_index: int, content_index: int, item_id: string, output_index: int} * * @implements ResponseContract */ @@ -34,7 +36,7 @@ final class OutputTextAnnotationAdded implements ResponseContract, ResponseHasMe use HasMetaInformation; private function __construct( - public readonly OutputMessageContentOutputTextAnnotationsFileCitation|OutputMessageContentOutputTextAnnotationsFilePath|OutputMessageContentOutputTextAnnotationsUrlCitation $annotation, + public readonly OutputMessageContentOutputTextAnnotationsContainerFile|OutputMessageContentOutputTextAnnotationsFileCitation|OutputMessageContentOutputTextAnnotationsFilePath|OutputMessageContentOutputTextAnnotationsUrlCitation $annotation, public readonly int $annotationIndex, public readonly int $contentIndex, public readonly string $itemId, @@ -47,11 +49,19 @@ private function __construct( */ public static function from(array $attributes, MetaInformation $meta): self { - $annotation = match ($attributes['annotation']['type']) { - 'file_citation' => OutputMessageContentOutputTextAnnotationsFileCitation::from($attributes['annotation']), - 'file_path' => OutputMessageContentOutputTextAnnotationsFilePath::from($attributes['annotation']), - 'url_citation' => OutputMessageContentOutputTextAnnotationsUrlCitation::from($attributes['annotation']), - }; + $annotationType = $attributes['annotation']['type'] ?? 'unknown'; + + // Handle container_file types with any suffix + if (str_starts_with($annotationType, 'container_file')) { + $annotation = OutputMessageContentOutputTextAnnotationsContainerFile::from($attributes['annotation']); + } else { + $annotation = match ($annotationType) { + 'file_citation' => OutputMessageContentOutputTextAnnotationsFileCitation::from($attributes['annotation']), + 'file_path' => OutputMessageContentOutputTextAnnotationsFilePath::from($attributes['annotation']), + 'url_citation' => OutputMessageContentOutputTextAnnotationsUrlCitation::from($attributes['annotation']), + default => throw new \UnhandledMatchError("Unhandled annotation type: {$annotationType}") + }; + } return new self( annotation: $annotation, diff --git a/src/Responses/Responses/Tool/CodeInterpreterTool.php b/src/Responses/Responses/Tool/CodeInterpreterTool.php new file mode 100644 index 00000000..cc354ceb --- /dev/null +++ b/src/Responses/Responses/Tool/CodeInterpreterTool.php @@ -0,0 +1,60 @@ +}|string + * @phpstan-type CodeInterpreterToolType array{type: 'code_interpreter', container?: CodeInterpreterContainerType} + * + * @implements ResponseContract + */ +final class CodeInterpreterTool implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array{type: string, files?: array}|string|null $container + */ + private function __construct( + public readonly string $type, + public readonly array|string|null $container, + ) {} + + /** + * @param CodeInterpreterToolType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + container: $attributes['container'] ?? null, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + $result = [ + 'type' => $this->type, + ]; + + if ($this->container !== null) { + $result['container'] = $this->container; + } + + return $result; + } +} \ No newline at end of file From 1e0e12a46ab76efe2a34d6be5595c1ba80634fa3 Mon Sep 17 00:00:00 2001 From: Michael Sitarzewski Date: Sat, 21 Jun 2025 20:32:59 -0500 Subject: [PATCH 2/3] refactor: improve type safety and add static analysis annotations for code interpreter responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit composer test now reports: • 896 PHPUnit tests PASS • Laravel Pint style PASS • php-stan level passes with zero errors. --- src/Responses/Responses/CreateResponse.php | 2 +- .../Output/OutputCodeInterpreterToolCall.php | 2 +- .../Output/OutputMessageContentOutputText.php | 26 ++++++++++++++----- ...tentOutputTextAnnotationsContainerFile.php | 10 +++---- .../Streaming/CodeInterpreterCall.php | 2 +- .../Streaming/CodeInterpreterCodeDelta.php | 2 +- .../Streaming/CodeInterpreterCodeDone.php | 2 +- .../Streaming/OutputTextAnnotationAdded.php | 23 +++++++++++----- .../Responses/Tool/CodeInterpreterTool.php | 7 +++-- 9 files changed, 51 insertions(+), 25 deletions(-) diff --git a/src/Responses/Responses/CreateResponse.php b/src/Responses/Responses/CreateResponse.php index 9988f806..3c6ee2d5 100644 --- a/src/Responses/Responses/CreateResponse.php +++ b/src/Responses/Responses/CreateResponse.php @@ -21,11 +21,11 @@ use OpenAI\Responses\Responses\Output\OutputMessageContentOutputText; use OpenAI\Responses\Responses\Output\OutputReasoning; 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\CodeInterpreterTool; use OpenAI\Responses\Responses\Tool\RemoteMcpTool; use OpenAI\Responses\Responses\Tool\WebSearchTool; use OpenAI\Responses\Responses\ToolChoice\FunctionToolChoice; diff --git a/src/Responses/Responses/Output/OutputCodeInterpreterToolCall.php b/src/Responses/Responses/Output/OutputCodeInterpreterToolCall.php index f783b673..f0031299 100644 --- a/src/Responses/Responses/Output/OutputCodeInterpreterToolCall.php +++ b/src/Responses/Responses/Output/OutputCodeInterpreterToolCall.php @@ -54,4 +54,4 @@ public function toArray(): array 'type' => $this->type, ]; } -} \ No newline at end of file +} diff --git a/src/Responses/Responses/Output/OutputMessageContentOutputText.php b/src/Responses/Responses/Output/OutputMessageContentOutputText.php index 21ae7915..8c642fef 100644 --- a/src/Responses/Responses/Output/OutputMessageContentOutputText.php +++ b/src/Responses/Responses/Output/OutputMessageContentOutputText.php @@ -48,17 +48,27 @@ public static function from(array $attributes): self { $annotations = array_map( function (array $annotation): AnnotationContainerFile|AnnotationFileCitation|AnnotationFilePath|AnnotationUrlCitation { - $annotationType = $annotation['type'] ?? 'unknown'; - + $annotationType = $annotation['type']; + // Handle container_file types with any suffix if (str_starts_with($annotationType, 'container_file')) { + /** @var ContainerFileType $annotation */ return AnnotationContainerFile::from($annotation); } - + return match ($annotationType) { - 'file_citation' => AnnotationFileCitation::from($annotation), - 'file_path' => AnnotationFilePath::from($annotation), - 'url_citation' => AnnotationUrlCitation::from($annotation), + 'file_citation' => (static function (array $annotation) { + /** @var FileCitationType $annotation */ + return AnnotationFileCitation::from($annotation); + })($annotation), + 'file_path' => (static function (array $annotation) { + /** @var FilePathType $annotation */ + return AnnotationFilePath::from($annotation); + })($annotation), + 'url_citation' => (static function (array $annotation) { + /** @var UrlCitationType $annotation */ + return AnnotationUrlCitation::from($annotation); + })($annotation), default => throw new \UnhandledMatchError("Unhandled annotation type: {$annotationType}") }; }, @@ -79,7 +89,9 @@ public function toArray(): array { return [ 'annotations' => array_map( - fn (AnnotationContainerFile|AnnotationFileCitation|AnnotationFilePath|AnnotationUrlCitation $annotation): array => $annotation->toArray(), + static function (AnnotationContainerFile|AnnotationFileCitation|AnnotationFilePath|AnnotationUrlCitation $annotation): array { + return $annotation->toArray(); + }, $this->annotations, ), 'text' => $this->text, diff --git a/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsContainerFile.php b/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsContainerFile.php index efe3d977..dfa0b687 100644 --- a/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsContainerFile.php +++ b/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsContainerFile.php @@ -53,19 +53,19 @@ public function toArray(): array 'file_id' => $this->fileId, 'type' => $this->type, ]; - + if ($this->text !== null) { $result['text'] = $this->text; } - + if ($this->startIndex !== null) { $result['start_index'] = $this->startIndex; } - + if ($this->endIndex !== null) { $result['end_index'] = $this->endIndex; } - + return $result; } -} \ No newline at end of file +} diff --git a/src/Responses/Responses/Streaming/CodeInterpreterCall.php b/src/Responses/Responses/Streaming/CodeInterpreterCall.php index 908b64e6..59506526 100644 --- a/src/Responses/Responses/Streaming/CodeInterpreterCall.php +++ b/src/Responses/Responses/Streaming/CodeInterpreterCall.php @@ -54,4 +54,4 @@ public function toArray(): array 'output_index' => $this->outputIndex, ]; } -} \ No newline at end of file +} diff --git a/src/Responses/Responses/Streaming/CodeInterpreterCodeDelta.php b/src/Responses/Responses/Streaming/CodeInterpreterCodeDelta.php index 543ff56e..dc603f4e 100644 --- a/src/Responses/Responses/Streaming/CodeInterpreterCodeDelta.php +++ b/src/Responses/Responses/Streaming/CodeInterpreterCodeDelta.php @@ -57,4 +57,4 @@ public function toArray(): array 'output_index' => $this->outputIndex, ]; } -} \ No newline at end of file +} diff --git a/src/Responses/Responses/Streaming/CodeInterpreterCodeDone.php b/src/Responses/Responses/Streaming/CodeInterpreterCodeDone.php index d42bafdc..3dc25232 100644 --- a/src/Responses/Responses/Streaming/CodeInterpreterCodeDone.php +++ b/src/Responses/Responses/Streaming/CodeInterpreterCodeDone.php @@ -57,4 +57,4 @@ public function toArray(): array 'output_index' => $this->outputIndex, ]; } -} \ No newline at end of file +} diff --git a/src/Responses/Responses/Streaming/OutputTextAnnotationAdded.php b/src/Responses/Responses/Streaming/OutputTextAnnotationAdded.php index a80dd57d..10a9459f 100644 --- a/src/Responses/Responses/Streaming/OutputTextAnnotationAdded.php +++ b/src/Responses/Responses/Streaming/OutputTextAnnotationAdded.php @@ -49,16 +49,27 @@ private function __construct( */ public static function from(array $attributes, MetaInformation $meta): self { - $annotationType = $attributes['annotation']['type'] ?? 'unknown'; - + $annotationType = $attributes['annotation']['type']; + // Handle container_file types with any suffix if (str_starts_with($annotationType, 'container_file')) { - $annotation = OutputMessageContentOutputTextAnnotationsContainerFile::from($attributes['annotation']); + /** @var ContainerFileType $containerFileAttributes */ + $containerFileAttributes = $attributes['annotation']; + $annotation = OutputMessageContentOutputTextAnnotationsContainerFile::from($containerFileAttributes); } else { $annotation = match ($annotationType) { - 'file_citation' => OutputMessageContentOutputTextAnnotationsFileCitation::from($attributes['annotation']), - 'file_path' => OutputMessageContentOutputTextAnnotationsFilePath::from($attributes['annotation']), - 'url_citation' => OutputMessageContentOutputTextAnnotationsUrlCitation::from($attributes['annotation']), + 'file_citation' => (static function (array $ann) { + /** @var FileCitationType $ann */ + return OutputMessageContentOutputTextAnnotationsFileCitation::from($ann); + })($attributes['annotation']), + 'file_path' => (static function (array $ann) { + /** @var FilePathType $ann */ + return OutputMessageContentOutputTextAnnotationsFilePath::from($ann); + })($attributes['annotation']), + 'url_citation' => (static function (array $ann) { + /** @var UrlCitationType $ann */ + return OutputMessageContentOutputTextAnnotationsUrlCitation::from($ann); + })($attributes['annotation']), default => throw new \UnhandledMatchError("Unhandled annotation type: {$annotationType}") }; } diff --git a/src/Responses/Responses/Tool/CodeInterpreterTool.php b/src/Responses/Responses/Tool/CodeInterpreterTool.php index cc354ceb..94a64a8d 100644 --- a/src/Responses/Responses/Tool/CodeInterpreterTool.php +++ b/src/Responses/Responses/Tool/CodeInterpreterTool.php @@ -27,6 +27,9 @@ final class CodeInterpreterTool implements ResponseContract * @param array{type: string, files?: array}|string|null $container */ private function __construct( + /** + * @var 'code_interpreter' + */ public readonly string $type, public readonly array|string|null $container, ) {} @@ -48,7 +51,7 @@ public static function from(array $attributes): self public function toArray(): array { $result = [ - 'type' => $this->type, + 'type' => 'code_interpreter', ]; if ($this->container !== null) { @@ -57,4 +60,4 @@ public function toArray(): array return $result; } -} \ No newline at end of file +} From bb8e13361235b58d10653b29889887f500619c3d Mon Sep 17 00:00:00 2001 From: Michael Sitarzewski Date: Tue, 24 Jun 2025 10:04:02 -0500 Subject: [PATCH 3/3] feat: add support for parsing various MCP call error formats test: enhance error handling tests for OutputMcpCall response --- .../Responses/Output/OutputMcpCall.php | 40 ++++++- .../Responses/Output/OutputMcpCall.php | 100 ++++++++++++++++++ 2 files changed, 137 insertions(+), 3 deletions(-) diff --git a/src/Responses/Responses/Output/OutputMcpCall.php b/src/Responses/Responses/Output/OutputMcpCall.php index 9436cc6b..891dd7ec 100644 --- a/src/Responses/Responses/Output/OutputMcpCall.php +++ b/src/Responses/Responses/Output/OutputMcpCall.php @@ -9,7 +9,7 @@ use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @phpstan-type OutputMcpCallType array{id: string, server_label: string, type: 'mcp_call', approval_request_id: ?string, arguments: string, error: ?string, name: string, output: ?string} + * @phpstan-type OutputMcpCallType array{id: string, server_label: string, type: 'mcp_call', approval_request_id: ?string, arguments: string, error?: mixed, name: string, output?: ?string} * * @implements ResponseContract */ @@ -41,6 +41,40 @@ private function __construct( */ public static function from(array $attributes): self { + // Handle error field which might be a string or an MCP content array + $error = $attributes['error'] ?? null; + $extractedError = null; + + if (is_array($error)) { + // OpenAI might be passing through the MCP content array format + + // Check if it's a direct content array [{type: 'text', text: '...'}] + if (isset($error[0])) { + /** @var array $errorArray */ + $errorArray = $error; + foreach ($errorArray as $content) { + if (is_array($content) && isset($content['type']) && $content['type'] === 'text' && isset($content['text'])) { + $extractedError = $content['text']; + break; + } + } + } + // Check if it has a content property {content: [{type: 'text', text: '...'}]} + elseif (isset($error['content']) && is_array($error['content'])) { + foreach ($error['content'] as $content) { + if (is_array($content) && isset($content['type']) && $content['type'] === 'text' && isset($content['text'])) { + $extractedError = $content['text']; + break; + } + } + } + + // Fallback to JSON encoding if we can't extract the text + $extractedError = $extractedError ?? json_encode($error, JSON_THROW_ON_ERROR); + } else { + $extractedError = $error; + } + return new self( id: $attributes['id'], serverLabel: $attributes['server_label'], @@ -48,8 +82,8 @@ public static function from(array $attributes): self arguments: $attributes['arguments'], name: $attributes['name'], approvalRequestId: $attributes['approval_request_id'], - error: $attributes['error'], - output: $attributes['output'], + error: $extractedError, + output: $attributes['output'] ?? null, ); } diff --git a/tests/Responses/Responses/Output/OutputMcpCall.php b/tests/Responses/Responses/Output/OutputMcpCall.php index 68e49266..fa0e6b63 100644 --- a/tests/Responses/Responses/Output/OutputMcpCall.php +++ b/tests/Responses/Responses/Output/OutputMcpCall.php @@ -2,6 +2,8 @@ use OpenAI\Responses\Responses\Output\OutputMcpCall; +require_once __DIR__.'/../../../Fixtures/Responses.php'; + test('from', function () { $response = OutputMcpCall::from(outputMcpCall()); @@ -30,3 +32,101 @@ ->toBeArray() ->toBe(outputMcpCall()); }); + +test('from with error as direct content array', function () { + $errorResponse = [ + 'id' => 'mcp_error_123', + 'server_label' => 'test-server', + 'type' => 'mcp_call', + 'arguments' => '{"foo": "bar"}', + 'name' => 'testFunction', + 'approval_request_id' => null, + 'error' => [ + [ + 'type' => 'text', + 'text' => 'Error: Function execution failed', + ], + ], + 'output' => null, + ]; + + $response = OutputMcpCall::from($errorResponse); + + expect($response) + ->toBeInstanceOf(OutputMcpCall::class) + ->id->toBe('mcp_error_123') + ->serverLabel->toBe('test-server') + ->name->toBe('testFunction') + ->arguments->toBe('{"foo": "bar"}') + ->type->toBe('mcp_call') + ->approvalRequestId->toBeNull() + ->error->toBe('Error: Function execution failed') + ->output->toBeNull(); +}); + +test('from with error as object with content array', function () { + $errorResponse = [ + 'id' => 'mcp_error_456', + 'server_label' => 'test-server', + 'type' => 'mcp_call', + 'arguments' => '{"foo": "bar"}', + 'name' => 'testFunction', + 'approval_request_id' => null, + 'error' => [ + 'content' => [ + [ + 'type' => 'text', + 'text' => 'Error: Permission denied', + ], + ], + ], + 'output' => null, + ]; + + $response = OutputMcpCall::from($errorResponse); + + expect($response) + ->toBeInstanceOf(OutputMcpCall::class) + ->error->toBe('Error: Permission denied'); +}); + +test('from with error as string', function () { + $errorResponse = [ + 'id' => 'string_error_789', + 'server_label' => 'test-server', + 'type' => 'mcp_call', + 'arguments' => '{"foo": "bar"}', + 'name' => 'testFunction', + 'approval_request_id' => null, + 'error' => 'Simple error message', + 'output' => null, + ]; + + $response = OutputMcpCall::from($errorResponse); + + expect($response) + ->toBeInstanceOf(OutputMcpCall::class) + ->error->toBe('Simple error message'); +}); + +test('from with unparseable error array', function () { + $errorResponse = [ + 'id' => 'complex_error', + 'server_label' => 'test-server', + 'type' => 'mcp_call', + 'arguments' => '{}', + 'name' => 'testFunction', + 'approval_request_id' => null, + 'error' => [ + 'some' => 'complex', + 'nested' => ['error' => 'structure'], + ], + 'output' => null, + ]; + + $response = OutputMcpCall::from($errorResponse); + + expect($response) + ->toBeInstanceOf(OutputMcpCall::class) + ->error->toBe('{"some":"complex","nested":{"error":"structure"}}'); +});