Skip to content

Commit 0a29b39

Browse files
authored
feat(OpenAI): Add streaming for ImageGeneration (Responses API) (#602)
* feat(OpenAI): Add streaming for ImageGeneration (Responses API) * fix(OpenAI): `model` is missing on stream and partial images is default 0 * test(OpenAI): image generation steaming test * fix(OpenAI): add output item (Image Generation) * fix(OpenAI): add output item (Image Generation) * test(OpenAI): enhanced stream testing * fix(OpenAI): image generation streaming fixes * fix(OpenAI): proper type * chore(OpenAI): align test to stream mock * fix(OpenAI): result is missing on partial image failure
1 parent 7e20d19 commit 0a29b39

11 files changed

+305
-15
lines changed

src/Responses/Responses/CreateResponse.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use OpenAI\Responses\Responses\Output\OutputComputerToolCall;
1313
use OpenAI\Responses\Responses\Output\OutputFileSearchToolCall;
1414
use OpenAI\Responses\Responses\Output\OutputFunctionToolCall;
15+
use OpenAI\Responses\Responses\Output\OutputImageGenerationToolCall;
1516
use OpenAI\Responses\Responses\Output\OutputMessage;
1617
use OpenAI\Responses\Responses\Output\OutputMessageContentOutputText;
1718
use OpenAI\Responses\Responses\Output\OutputReasoning;
@@ -33,6 +34,7 @@
3334
* @phpstan-import-type OutputMessageType from OutputMessage
3435
* @phpstan-import-type OutputReasoningType from OutputReasoning
3536
* @phpstan-import-type OutputWebSearchToolCallType from OutputWebSearchToolCall
37+
* @phpstan-import-type OutputImageGenerationToolCallType from OutputImageGenerationToolCall
3638
* @phpstan-import-type ComputerUseToolType from ComputerUseTool
3739
* @phpstan-import-type FileSearchToolType from FileSearchTool
3840
* @phpstan-import-type ImageGenerationToolType from ImageGenerationTool
@@ -47,7 +49,7 @@
4749
*
4850
* @phpstan-type ToolChoiceType 'none'|'auto'|'required'|FunctionToolChoiceType|HostedToolChoiceType
4951
* @phpstan-type ToolsType array<int, ComputerUseToolType|FileSearchToolType|FunctionToolType|WebSearchToolType|ImageGenerationToolType>
50-
* @phpstan-type OutputType array<int, OutputComputerToolCallType|OutputFileSearchToolCallType|OutputFunctionToolCallType|OutputMessageType|OutputReasoningType|OutputWebSearchToolCallType>
52+
* @phpstan-type OutputType array<int, OutputComputerToolCallType|OutputFileSearchToolCallType|OutputFunctionToolCallType|OutputMessageType|OutputReasoningType|OutputWebSearchToolCallType|OutputImageGenerationToolCallType>
5153
* @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<string, string>|null}
5254
*
5355
* @implements ResponseContract<CreateResponseType>
@@ -65,7 +67,7 @@ final class CreateResponse implements ResponseContract, ResponseHasMetaInformati
6567
/**
6668
* @param 'response' $object
6769
* @param 'completed'|'failed'|'in_progress'|'incomplete' $status
68-
* @param array<int, OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning> $output
70+
* @param array<int, OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputImageGenerationToolCall> $output
6971
* @param array<int, ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool> $tools
7072
* @param 'auto'|'disabled'|null $truncation
7173
* @param array<string, string> $metadata
@@ -104,13 +106,14 @@ private function __construct(
104106
public static function from(array $attributes, MetaInformation $meta): self
105107
{
106108
$output = array_map(
107-
fn (array $output): OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning => match ($output['type']) {
109+
fn (array $output): OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputImageGenerationToolCall => match ($output['type']) {
108110
'message' => OutputMessage::from($output),
109111
'file_search_call' => OutputFileSearchToolCall::from($output),
110112
'function_call' => OutputFunctionToolCall::from($output),
111113
'web_search_call' => OutputWebSearchToolCall::from($output),
112114
'computer_call' => OutputComputerToolCall::from($output),
113115
'reasoning' => OutputReasoning::from($output),
116+
'image_generation_call' => OutputImageGenerationToolCall::from($output),
114117
},
115118
$attributes['output'],
116119
);
@@ -201,7 +204,7 @@ public function toArray(): array
201204
'metadata' => $this->metadata ?? [],
202205
'model' => $this->model,
203206
'output' => array_map(
204-
fn (OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning $output): array => $output->toArray(),
207+
fn (OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputImageGenerationToolCall $output): array => $output->toArray(),
205208
$this->output
206209
),
207210
'parallel_tool_calls' => $this->parallelToolCalls,

src/Responses/Responses/CreateStreamedResponse.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
use OpenAI\Responses\Responses\Streaming\FileSearchCall;
1313
use OpenAI\Responses\Responses\Streaming\FunctionCallArgumentsDelta;
1414
use OpenAI\Responses\Responses\Streaming\FunctionCallArgumentsDone;
15+
use OpenAI\Responses\Responses\Streaming\ImageGenerationPart;
16+
use OpenAI\Responses\Responses\Streaming\ImageGenerationPartialImage;
1517
use OpenAI\Responses\Responses\Streaming\OutputItem;
1618
use OpenAI\Responses\Responses\Streaming\OutputTextAnnotationAdded;
1719
use OpenAI\Responses\Responses\Streaming\OutputTextDelta;
@@ -40,7 +42,7 @@ final class CreateStreamedResponse implements ResponseContract
4042

4143
private function __construct(
4244
public readonly string $event,
43-
public readonly CreateResponse|OutputItem|ContentPart|OutputTextDelta|OutputTextAnnotationAdded|OutputTextDone|RefusalDelta|RefusalDone|FunctionCallArgumentsDelta|FunctionCallArgumentsDone|FileSearchCall|WebSearchCall|ReasoningSummaryPart|ReasoningSummaryTextDelta|ReasoningSummaryTextDone|Error $response,
45+
public readonly CreateResponse|OutputItem|ContentPart|OutputTextDelta|OutputTextAnnotationAdded|OutputTextDone|RefusalDelta|RefusalDone|FunctionCallArgumentsDelta|FunctionCallArgumentsDone|FileSearchCall|WebSearchCall|ReasoningSummaryPart|ReasoningSummaryTextDelta|ReasoningSummaryTextDone|ImageGenerationPart|ImageGenerationPartialImage|Error $response,
4446
) {}
4547

4648
/**
@@ -79,6 +81,10 @@ public static function from(array $attributes): self
7981
'response.reasoning_summary_part.done' => ReasoningSummaryPart::from($attributes, $meta), // @phpstan-ignore-line
8082
'response.reasoning_summary_text.delta' => ReasoningSummaryTextDelta::from($attributes, $meta), // @phpstan-ignore-line
8183
'response.reasoning_summary_text.done' => ReasoningSummaryTextDone::from($attributes, $meta), // @phpstan-ignore-line
84+
'response.image_generation_call.completed',
85+
'response.image_generation_call.generating',
86+
'response.image_generation_call.in_progress' => ImageGenerationPart::from($attributes, $meta), // @phpstan-ignore-line
87+
'response.image_generation_call.partial_image' => ImageGenerationPartialImage::from($attributes, $meta), // @phpstan-ignore-line
8288
'error' => Error::from($attributes, $meta), // @phpstan-ignore-line
8389
default => throw new UnknownEventException('Unknown Responses streaming event: '.$event),
8490
};
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenAI\Responses\Responses\Output;
6+
7+
use OpenAI\Contracts\ResponseContract;
8+
use OpenAI\Responses\Concerns\ArrayAccessible;
9+
use OpenAI\Testing\Responses\Concerns\Fakeable;
10+
11+
/**
12+
* @phpstan-type OutputImageGenerationToolCallType array{id: string, result?: string|null, status: string, type: 'image_generation_call'}
13+
*
14+
* @implements ResponseContract<OutputImageGenerationToolCallType>
15+
*/
16+
final class OutputImageGenerationToolCall implements ResponseContract
17+
{
18+
/**
19+
* @use ArrayAccessible<OutputImageGenerationToolCallType>
20+
*/
21+
use ArrayAccessible;
22+
23+
use Fakeable;
24+
25+
/**
26+
* @param 'image_generation_call' $type
27+
*/
28+
private function __construct(
29+
public readonly string $id,
30+
public readonly ?string $result,
31+
public readonly string $status,
32+
public readonly string $type,
33+
) {}
34+
35+
/**
36+
* @param OutputImageGenerationToolCallType $attributes
37+
*/
38+
public static function from(array $attributes): self
39+
{
40+
return new self(
41+
id: $attributes['id'],
42+
result: $attributes['result'] ?? null,
43+
status: $attributes['status'],
44+
type: $attributes['type'],
45+
);
46+
}
47+
48+
/**
49+
* {@inheritDoc}
50+
*/
51+
public function toArray(): array
52+
{
53+
return [
54+
'id' => $this->id,
55+
'result' => $this->result,
56+
'status' => $this->status,
57+
'type' => $this->type,
58+
];
59+
}
60+
}

src/Responses/Responses/RetrieveResponse.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use OpenAI\Responses\Responses\Output\OutputComputerToolCall;
1313
use OpenAI\Responses\Responses\Output\OutputFileSearchToolCall;
1414
use OpenAI\Responses\Responses\Output\OutputFunctionToolCall;
15+
use OpenAI\Responses\Responses\Output\OutputImageGenerationToolCall;
1516
use OpenAI\Responses\Responses\Output\OutputMessage;
1617
use OpenAI\Responses\Responses\Output\OutputReasoning;
1718
use OpenAI\Responses\Responses\Output\OutputWebSearchToolCall;
@@ -32,6 +33,7 @@
3233
* @phpstan-import-type OutputMessageType from OutputMessage
3334
* @phpstan-import-type OutputReasoningType from OutputReasoning
3435
* @phpstan-import-type OutputWebSearchToolCallType from OutputWebSearchToolCall
36+
* @phpstan-import-type OutputImageGenerationToolCallType from OutputImageGenerationToolCall
3537
* @phpstan-import-type ComputerUseToolType from ComputerUseTool
3638
* @phpstan-import-type FileSearchToolType from FileSearchTool
3739
* @phpstan-import-type ImageGenerationToolType from ImageGenerationTool
@@ -46,7 +48,7 @@
4648
*
4749
* @phpstan-type ToolChoiceType 'none'|'auto'|'required'|FunctionToolChoiceType|HostedToolChoiceType
4850
* @phpstan-type ToolsType array<int, ComputerUseToolType|FileSearchToolType|FunctionToolType|WebSearchToolType|ImageGenerationToolType>
49-
* @phpstan-type OutputType array<int, OutputComputerToolCallType|OutputFileSearchToolCallType|OutputFunctionToolCallType|OutputMessageType|OutputReasoningType|OutputWebSearchToolCallType>
51+
* @phpstan-type OutputType array<int, OutputComputerToolCallType|OutputFileSearchToolCallType|OutputFunctionToolCallType|OutputMessageType|OutputReasoningType|OutputWebSearchToolCallType|OutputImageGenerationToolCallType>
5052
* @phpstan-type RetrieveResponseType 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, 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<string, string>|null}
5153
*
5254
* @implements ResponseContract<RetrieveResponseType>
@@ -64,7 +66,7 @@ final class RetrieveResponse implements ResponseContract, ResponseHasMetaInforma
6466
/**
6567
* @param 'response' $object
6668
* @param 'completed'|'failed'|'in_progress'|'incomplete' $status
67-
* @param array<int, OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning> $output
69+
* @param array<int, OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputImageGenerationToolCall> $output
6870
* @param array<int, ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool|ImageGenerationTool> $tools
6971
* @param 'auto'|'disabled'|null $truncation
7072
* @param array<string, string> $metadata
@@ -102,13 +104,14 @@ private function __construct(
102104
public static function from(array $attributes, MetaInformation $meta): self
103105
{
104106
$output = array_map(
105-
fn (array $output): OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning => match ($output['type']) {
107+
fn (array $output): OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputImageGenerationToolCall => match ($output['type']) {
106108
'message' => OutputMessage::from($output),
107109
'file_search_call' => OutputFileSearchToolCall::from($output),
108110
'function_call' => OutputFunctionToolCall::from($output),
109111
'web_search_call' => OutputWebSearchToolCall::from($output),
110112
'computer_call' => OutputComputerToolCall::from($output),
111113
'reasoning' => OutputReasoning::from($output),
114+
'image_generation_call' => OutputImageGenerationToolCall::from($output),
112115
},
113116
$attributes['output'],
114117
);
@@ -186,7 +189,7 @@ public function toArray(): array
186189
'metadata' => $this->metadata ?? [],
187190
'model' => $this->model,
188191
'output' => array_map(
189-
fn (OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning $output): array => $output->toArray(),
192+
fn (OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning|OutputImageGenerationToolCall $output): array => $output->toArray(),
190193
$this->output
191194
),
192195
'parallel_tool_calls' => $this->parallelToolCalls,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenAI\Responses\Responses\Streaming;
6+
7+
use OpenAI\Contracts\ResponseContract;
8+
use OpenAI\Contracts\ResponseHasMetaInformationContract;
9+
use OpenAI\Responses\Concerns\ArrayAccessible;
10+
use OpenAI\Responses\Concerns\HasMetaInformation;
11+
use OpenAI\Responses\Meta\MetaInformation;
12+
use OpenAI\Testing\Responses\Concerns\Fakeable;
13+
14+
/**
15+
* @phpstan-type ImageGenerationPartType array{output_index: int, item_id: string, sequence_number: int}
16+
*
17+
* @implements ResponseContract<ImageGenerationPartType>
18+
*/
19+
final class ImageGenerationPart implements ResponseContract, ResponseHasMetaInformationContract
20+
{
21+
/**
22+
* @use ArrayAccessible<ImageGenerationPartType>
23+
*/
24+
use ArrayAccessible;
25+
26+
use Fakeable;
27+
use HasMetaInformation;
28+
29+
private function __construct(
30+
public readonly int $outputIndex,
31+
public readonly string $itemId,
32+
public readonly int $sequenceNumber,
33+
private readonly MetaInformation $meta,
34+
) {}
35+
36+
/**
37+
* @param ImageGenerationPartType $attributes
38+
*/
39+
public static function from(array $attributes, MetaInformation $meta): self
40+
{
41+
return new self(
42+
outputIndex: $attributes['output_index'],
43+
itemId: $attributes['item_id'],
44+
sequenceNumber: $attributes['sequence_number'],
45+
meta: $meta,
46+
);
47+
}
48+
49+
/**
50+
* {@inheritDoc}
51+
*/
52+
public function toArray(): array
53+
{
54+
return [
55+
'output_index' => $this->outputIndex,
56+
'item_id' => $this->itemId,
57+
'sequence_number' => $this->sequenceNumber,
58+
];
59+
}
60+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenAI\Responses\Responses\Streaming;
6+
7+
use OpenAI\Contracts\ResponseContract;
8+
use OpenAI\Contracts\ResponseHasMetaInformationContract;
9+
use OpenAI\Responses\Concerns\ArrayAccessible;
10+
use OpenAI\Responses\Concerns\HasMetaInformation;
11+
use OpenAI\Responses\Meta\MetaInformation;
12+
use OpenAI\Testing\Responses\Concerns\Fakeable;
13+
14+
/**
15+
* @phpstan-type ImageGenerationPartialImageType array{output_index: int, item_id: string, sequence_number: int, partial_image_index: int, partial_image_b64: string}
16+
*
17+
* @implements ResponseContract<ImageGenerationPartialImageType>
18+
*/
19+
final class ImageGenerationPartialImage implements ResponseContract, ResponseHasMetaInformationContract
20+
{
21+
/**
22+
* @use ArrayAccessible<ImageGenerationPartialImageType>
23+
*/
24+
use ArrayAccessible;
25+
26+
use Fakeable;
27+
use HasMetaInformation;
28+
29+
private function __construct(
30+
public readonly int $outputIndex,
31+
public readonly string $itemId,
32+
public readonly int $sequenceNumber,
33+
public readonly int $partialImageIndex,
34+
public readonly string $partialImageB64,
35+
private readonly MetaInformation $meta,
36+
) {}
37+
38+
/**
39+
* @param ImageGenerationPartialImageType $attributes
40+
*/
41+
public static function from(array $attributes, MetaInformation $meta): self
42+
{
43+
return new self(
44+
outputIndex: $attributes['output_index'],
45+
itemId: $attributes['item_id'],
46+
sequenceNumber: $attributes['sequence_number'],
47+
partialImageIndex: $attributes['partial_image_index'],
48+
partialImageB64: $attributes['partial_image_b64'],
49+
meta: $meta,
50+
);
51+
}
52+
53+
/**
54+
* {@inheritDoc}
55+
*/
56+
public function toArray(): array
57+
{
58+
return [
59+
'output_index' => $this->outputIndex,
60+
'item_id' => $this->itemId,
61+
'sequence_number' => $this->sequenceNumber,
62+
'partial_image_index' => $this->partialImageIndex,
63+
'partial_image_b64' => $this->partialImageB64,
64+
];
65+
}
66+
}

src/Responses/Responses/Streaming/OutputItem.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use OpenAI\Responses\Responses\Output\OutputComputerToolCall;
1313
use OpenAI\Responses\Responses\Output\OutputFileSearchToolCall;
1414
use OpenAI\Responses\Responses\Output\OutputFunctionToolCall;
15+
use OpenAI\Responses\Responses\Output\OutputImageGenerationToolCall;
1516
use OpenAI\Responses\Responses\Output\OutputMessage;
1617
use OpenAI\Responses\Responses\Output\OutputReasoning;
1718
use OpenAI\Responses\Responses\Output\OutputWebSearchToolCall;
@@ -24,8 +25,9 @@
2425
* @phpstan-import-type OutputMessageType from OutputMessage
2526
* @phpstan-import-type OutputReasoningType from OutputReasoning
2627
* @phpstan-import-type OutputWebSearchToolCallType from OutputWebSearchToolCall
28+
* @phpstan-import-type OutputImageGenerationToolCallType from OutputImageGenerationToolCall
2729
*
28-
* @phpstan-type OutputItemType array{item: OutputComputerToolCallType|OutputFileSearchToolCallType|OutputFunctionToolCallType|OutputMessageType|OutputReasoningType|OutputWebSearchToolCallType, output_index: int}
30+
* @phpstan-type OutputItemType array{item: OutputComputerToolCallType|OutputFileSearchToolCallType|OutputFunctionToolCallType|OutputMessageType|OutputReasoningType|OutputWebSearchToolCallType|OutputImageGenerationToolCallType, output_index: int}
2931
*
3032
* @implements ResponseContract<OutputItemType>
3133
*/
@@ -41,7 +43,7 @@ final class OutputItem implements ResponseContract, ResponseHasMetaInformationCo
4143

4244
private function __construct(
4345
public readonly int $outputIndex,
44-
public readonly OutputMessage|OutputFileSearchToolCall|OutputFunctionToolCall|OutputWebSearchToolCall|OutputComputerToolCall|OutputReasoning $item,
46+
public readonly OutputMessage|OutputFileSearchToolCall|OutputFunctionToolCall|OutputWebSearchToolCall|OutputComputerToolCall|OutputReasoning|OutputImageGenerationToolCall $item,
4547
private readonly MetaInformation $meta,
4648
) {}
4749

@@ -57,6 +59,7 @@ public static function from(array $attributes, MetaInformation $meta): self
5759
'web_search_call' => OutputWebSearchToolCall::from($attributes['item']),
5860
'computer_call' => OutputComputerToolCall::from($attributes['item']),
5961
'reasoning' => OutputReasoning::from($attributes['item']),
62+
'image_generation_call' => OutputImageGenerationToolCall::from($attributes['item']),
6063
};
6164

6265
return new self(

0 commit comments

Comments
 (0)