diff --git a/src/Responses/Responses/CreateResponse.php b/src/Responses/Responses/CreateResponse.php index f2c31a4f..3c6ee2d5 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; @@ -20,6 +21,7 @@ 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; @@ -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..f0031299 --- /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, + ]; + } +} 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/src/Responses/Responses/Output/OutputMessageContentOutputText.php b/src/Responses/Responses/Output/OutputMessageContentOutputText.php index 77eedbfe..8c642fef 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,30 @@ 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']; + + // 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' => (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}") + }; }, $attributes['annotations'], ); @@ -67,7 +89,9 @@ public function toArray(): array { return [ 'annotations' => array_map( - fn (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 new file mode 100644 index 00000000..dfa0b687 --- /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; + } +} diff --git a/src/Responses/Responses/Streaming/CodeInterpreterCall.php b/src/Responses/Responses/Streaming/CodeInterpreterCall.php new file mode 100644 index 00000000..59506526 --- /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, + ]; + } +} diff --git a/src/Responses/Responses/Streaming/CodeInterpreterCodeDelta.php b/src/Responses/Responses/Streaming/CodeInterpreterCodeDelta.php new file mode 100644 index 00000000..dc603f4e --- /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, + ]; + } +} diff --git a/src/Responses/Responses/Streaming/CodeInterpreterCodeDone.php b/src/Responses/Responses/Streaming/CodeInterpreterCodeDone.php new file mode 100644 index 00000000..3dc25232 --- /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, + ]; + } +} 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..10a9459f 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,30 @@ 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']; + + // Handle container_file types with any suffix + if (str_starts_with($annotationType, 'container_file')) { + /** @var ContainerFileType $containerFileAttributes */ + $containerFileAttributes = $attributes['annotation']; + $annotation = OutputMessageContentOutputTextAnnotationsContainerFile::from($containerFileAttributes); + } else { + $annotation = match ($annotationType) { + '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}") + }; + } 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..94a64a8d --- /dev/null +++ b/src/Responses/Responses/Tool/CodeInterpreterTool.php @@ -0,0 +1,63 @@ +}|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( + /** + * @var 'code_interpreter' + */ + 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' => 'code_interpreter', + ]; + + if ($this->container !== null) { + $result['container'] = $this->container; + } + + return $result; + } +} 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"}}'); +});