diff --git a/README.md b/README.md index e59185f4..073de7b9 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ If you or your business relies on this package, it's important to support the de - [Get Started](#get-started) - [Usage](#usage) - [Models Resource](#models-resource) + - [Responses Resource](#responses-resource) - [Chat Resource](#chat-resource) - [Completions Resource](#completions-resource) - [Audio Resource](#audio-resource) @@ -154,6 +155,215 @@ $response->deleted; // true $response->toArray(); // ['id' => 'curie:ft-acmeco-2021-03-03-21-44-20', ...] ``` +### `Responses` Resource + +#### `create` + +Creates a model response. Provide text or image inputs to generate text or JSON outputs. Have the model call your own custom code or use built-in tools like web search or file search to use your own data as input for the model's response. + +```php +$response = $client->responses()->create([ + 'model' => 'gpt-4o-mini', + 'tools' => [ + [ + 'type' => 'web_search_preview' + ] + ], + 'input' => "what was a positive news story from today?", + 'temperature' => 0.7, + 'max_output_tokens' => 150, + 'tool_choice' => 'auto', + 'parallel_tool_calls' => true, + 'store' => true, + 'metadata' => [ + 'user_id' => '123', + 'session_id' => 'abc456' + ] +]); + +$response->id; // 'resp_67ccd2bed1ec8190b14f964abc054267' +$response->object; // 'response' +$response->createdAt; // 1741476542 +$response->status; // 'completed' +$response->model; // 'gpt-4o-mini' + +// Access output content +foreach ($response->output as $output) { + $output->type; // 'message' + $output->id; // 'msg_67ccd2bf17f0819081ff3bb2cf6508e6' + $output->status; // 'completed' + $output->role; // 'assistant' + + foreach ($output->content as $content) { + $content->type; // 'output_text' + $content->text; // The response text + $content->annotations; // Any annotations in the response + } +} + +// Access usage information +$response->usage->inputTokens; // 36 +$response->usage->outputTokens; // 87 +$response->usage->totalTokens; // 123 + +$response->toArray(); // ['id' => 'resp_67ccd2bed1ec8190b14f964abc054267', ...] +``` + +#### `create streamed` + +When you create a Response with stream set to true, the server will emit server-sent events to the client as the Response is generated. + +```php +$stream = $client->responses()->createStreamed([ + 'model' => 'gpt-4o-mini', + 'tools' => [ + [ + 'type' => 'web_search_preview' + ] + ], + 'input' => "what was a positive news story from today?", + 'stream' => true +]); + +foreach ($stream as $response) { + $response->id; // 'resp_67ccd2bed1ec8190b14f964abc054267' + $response->object; // 'response' + $response->createdAt; // 1741476542 + + foreach ($response->output as $output) { + // Process streaming output + echo $output->content[0]->text; + } +} +``` + +### `retrieve` + +Retrieves a model response with the given ID. + +```php +$response = $client->responses()->retrieve('resp_67ccd2bed1ec8190b14f964abc054267'); + +$response->id; // 'resp_67ccd2bed1ec8190b14f964abc054267' +$response->object; // 'response' +$response->createdAt; // 1741476542 +$response->status; // 'completed' +$response->error; // null +$response->incompleteDetails; // null +$response->instructions; // null +$response->maxOutputTokens; // null +$response->model; // 'gpt-4o-2024-08-06' +$response->parallelToolCalls; // true +$response->previousResponseId; // null +$response->store; // true +$response->temperature; // 1.0 +$response->toolChoice; // 'auto' +$response->topP; // 1.0 +$response->truncation; // 'disabled' + +$response->toArray(); // ['id' => 'resp_67ccd2bed1ec8190b14f964abc054267', ...] +``` + +### `delete` + +Deletes a model response with the given ID. + +```php +$response = $client->responses()->delete('resp_67ccd2bed1ec8190b14f964abc054267'); + +$response->id; // 'resp_67ccd2bed1ec8190b14f964abc054267' +$response->object; // 'response' +$response->deleted; // true + +$response->toArray(); // ['id' => 'resp_67ccd2bed1ec8190b14f964abc054267', 'deleted' => true, ...] +``` + +### `list` + +Lists input items for a response with the given ID. + +```php +$response = $client->responses()->list('resp_67ccd2bed1ec8190b14f964abc054267', [ + 'limit' => 10, + 'order' => 'desc' +]); + +$response->object; // 'list' + +foreach ($response->data as $item) { + $item->type; // 'message' + $item->id; // Response item ID + $item->status; // 'completed' + $item->role; // 'user' or 'assistant' + + foreach ($item->content as $content) { + $content->type; // Content type + $content->text; // Content text + $content->annotations; // Content annotations + } +} + +$response->firstId; // First item ID in the list +$response->lastId; // Last item ID in the list +$response->hasMore; // Whether there are more items to fetch + +$response->toArray(); // ['object' => 'list', 'data' => [...], ...] +``` + +### `Completions` Resource + +#### `create` + +Creates a completion for the provided prompt and parameters. + +```php +$response = $client->completions()->create([ + 'model' => 'gpt-3.5-turbo-instruct', + 'prompt' => 'Say this is a test', + 'max_tokens' => 6, + 'temperature' => 0 +]); + +$response->id; // 'cmpl-uqkvlQyYK7bGYrRHQ0eXlWi7' +$response->object; // 'text_completion' +$response->created; // 1589478378 +$response->model; // 'gpt-3.5-turbo-instruct' + +foreach ($response->choices as $choice) { + $choice->text; // '\n\nThis is a test' + $choice->index; // 0 + $choice->logprobs; // null + $choice->finishReason; // 'length' or null +} + +$response->usage->promptTokens; // 5, +$response->usage->completionTokens; // 6, +$response->usage->totalTokens; // 11 + +$response->toArray(); // ['id' => 'cmpl-uqkvlQyYK7bGYrRHQ0eXlWi7', ...] +``` + +#### `create streamed` + +Creates a streamed completion for the provided prompt and parameters. + +```php +$stream = $client->completions()->createStreamed([ + 'model' => 'gpt-3.5-turbo-instruct', + 'prompt' => 'Hi', + 'max_tokens' => 10, + ]); + +foreach($stream as $response){ + $response->choices[0]->text; +} +// 1. iteration => 'I' +// 2. iteration => ' am' +// 3. iteration => ' very' +// 4. iteration => ' excited' +// ... +``` + ### `Chat` Resource #### `create` diff --git a/src/Client.php b/src/Client.php index c2547e13..3aca764f 100644 --- a/src/Client.php +++ b/src/Client.php @@ -21,6 +21,7 @@ use OpenAI\Resources\Images; use OpenAI\Resources\Models; use OpenAI\Resources\Moderations; +use OpenAI\Resources\Responses; use OpenAI\Resources\Threads; use OpenAI\Resources\VectorStores; @@ -34,6 +35,16 @@ public function __construct(private readonly TransporterContract $transporter) // .. } + /** + * Manage responses to assist models with tasks. + * + * @see https://platform.openai.com/docs/api-reference/responses + */ + public function responses(): Responses + { + return new Responses($this->transporter); + } + /** * Given a prompt, the model will return one or more predicted completions, and can also return the probabilities * of alternative tokens at each position. diff --git a/src/Contracts/ClientContract.php b/src/Contracts/ClientContract.php index ad018605..daf44729 100644 --- a/src/Contracts/ClientContract.php +++ b/src/Contracts/ClientContract.php @@ -15,6 +15,7 @@ use OpenAI\Contracts\Resources\ImagesContract; use OpenAI\Contracts\Resources\ModelsContract; use OpenAI\Contracts\Resources\ModerationsContract; +use OpenAI\Contracts\Resources\ResponsesContract; use OpenAI\Contracts\Resources\ThreadsContract; use OpenAI\Contracts\Resources\VectorStoresContract; @@ -28,6 +29,13 @@ interface ClientContract */ public function completions(): CompletionsContract; + /** + * Manage responses to assist models with tasks. + * + * @see https://platform.openai.com/docs/api-reference/responses + */ + public function responses(): ResponsesContract; + /** * Given a chat conversation, the model will return a chat completion response. * diff --git a/src/Contracts/Resources/ResponsesContract.php b/src/Contracts/Resources/ResponsesContract.php new file mode 100644 index 00000000..4d13d6ed --- /dev/null +++ b/src/Contracts/Resources/ResponsesContract.php @@ -0,0 +1,60 @@ + $parameters + */ + public function create(array $parameters): CreateResponse; + + /** + * Create a streamed response. + * + * @see https://platform.openai.com/docs/api-reference/responses/create + * + * @param array $parameters + * @return StreamResponse + */ + public function createStreamed(array $parameters): StreamResponse; + + /** + * Retrieves a model response with the given ID. + * + * @see https://platform.openai.com/docs/api-reference/responses/retrieve + */ + public function retrieve(string $id): RetrieveResponse; + + /** + * Deletes a model response with the given ID. + * + * @see https://platform.openai.com/docs/api-reference/responses/delete + */ + public function delete(string $id): DeleteResponse; + + /** + * Returns a list of input items for a given response. + * + * @see https://platform.openai.com/docs/api-reference/responses/input-items + * + * @param array $parameters + */ + public function list(string $id, array $parameters = []): ListInputItems; +} diff --git a/src/Resources/Responses.php b/src/Resources/Responses.php new file mode 100644 index 00000000..f7257eb0 --- /dev/null +++ b/src/Resources/Responses.php @@ -0,0 +1,113 @@ + $parameters + */ + public function create(array $parameters): CreateResponse + { + $this->ensureNotStreamed($parameters); + + $payload = Payload::create('responses', $parameters); + + /** @var Response $response */ + $response = $this->transporter->requestObject($payload); + + return CreateResponse::from($response->data(), $response->meta()); + } + + /** + * When you create a Response with stream set to true, + * the server will emit server-sent events to the client as the Response is generated. + * + * @see https://platform.openai.com/docs/api-reference/responses-streaming + * + * @param array $parameters + * @return StreamResponse + */ + public function createStreamed(array $parameters): StreamResponse + { + $parameters = $this->setStreamParameter($parameters); + + $payload = Payload::create('responses', $parameters); + + $response = $this->transporter->requestStream($payload); + + return new StreamResponse(CreateStreamedResponse::class, $response); + } + + /** + * Retrieves a model response with the given ID. + * + * @see https://platform.openai.com/docs/api-reference/responses/get + */ + public function retrieve(string $id): RetrieveResponse + { + $payload = Payload::retrieve('responses', $id); + + /** @var Response $response */ + $response = $this->transporter->requestObject($payload); + + return RetrieveResponse::from($response->data(), $response->meta()); + } + + /** + * Deletes a model response with the given ID. + * + * @see https://platform.openai.com/docs/api-reference/responses/delete + */ + public function delete(string $id): DeleteResponse + { + $payload = Payload::delete('responses', $id); + + /** @var Response $response */ + $response = $this->transporter->requestObject($payload); + + return DeleteResponse::from($response->data(), $response->meta()); + } + + /** + * Lists input items for a response with the given ID. + * + * @see https://platform.openai.com/docs/api-reference/responses/input-items + * + * @param array $parameters + */ + public function list(string $id, array $parameters = []): ListInputItems + { + $payload = Payload::list('responses/'.$id.'/input_items', $parameters); + + /** @var Response}>}>, first_id: ?string, last_id: ?string, has_more: bool}> $response */ + $response = $this->transporter->requestObject($payload); + + return ListInputItems::from($response->data(), $response->meta()); + } +} diff --git a/src/Responses/Responses/CreateResponse.php b/src/Responses/Responses/CreateResponse.php new file mode 100644 index 00000000..a803dadf --- /dev/null +++ b/src/Responses/Responses/CreateResponse.php @@ -0,0 +1,208 @@ + + * @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, 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 + */ +final class CreateResponse implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + /** + * @param 'response' $object + * @param 'completed'|'failed'|'in_progress'|'incomplete' $status + * @param array $output + * @param array $tools + * @param 'auto'|'disabled'|null $truncation + * @param array $metadata + */ + private function __construct( + public readonly string $id, + public readonly string $object, + public readonly int $createdAt, + public readonly string $status, + public readonly ?CreateResponseError $error, + public readonly ?CreateResponseIncompleteDetails $incompleteDetails, + public readonly ?string $instructions, + public readonly ?int $maxOutputTokens, + public readonly string $model, + public readonly array $output, + public readonly bool $parallelToolCalls, + public readonly ?string $previousResponseId, + public readonly ?CreateResponseReasoning $reasoning, + public readonly bool $store, + public readonly ?float $temperature, + public readonly CreateResponseFormat $text, + public readonly string|FunctionToolChoice|HostedToolChoice $toolChoice, + public readonly array $tools, + public readonly ?float $topP, + public readonly ?string $truncation, + public readonly ?CreateResponseUsage $usage, + public readonly ?string $user, + public readonly array $metadata, + private readonly MetaInformation $meta, + ) {} + + /** + * @param CreateResponseType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + $output = array_map( + fn (array $output): OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning => match ($output['type']) { + 'message' => OutputMessage::from($output), + 'file_search_call' => OutputFileSearchToolCall::from($output), + 'function_call' => OutputFunctionToolCall::from($output), + 'web_search_call' => OutputWebSearchToolCall::from($output), + 'computer_call' => OutputComputerToolCall::from($output), + 'reasoning' => OutputReasoning::from($output), + }, + $attributes['output'], + ); + + $toolChoice = is_array($attributes['tool_choice']) + ? match ($attributes['tool_choice']['type']) { + 'file_search', 'web_search_preview', 'computer_use_preview' => HostedToolChoice::from($attributes['tool_choice']), + 'function' => FunctionToolChoice::from($attributes['tool_choice']), + } + : $attributes['tool_choice']; + + $tools = array_map( + fn (array $tool): ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool => 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), + }, + $attributes['tools'], + ); + + return new self( + id: $attributes['id'], + object: $attributes['object'], + createdAt: $attributes['created_at'], + status: $attributes['status'], + error: isset($attributes['error']) + ? CreateResponseError::from($attributes['error']) + : null, + incompleteDetails: isset($attributes['incomplete_details']) + ? CreateResponseIncompleteDetails::from($attributes['incomplete_details']) + : null, + instructions: $attributes['instructions'], + maxOutputTokens: $attributes['max_output_tokens'], + model: $attributes['model'], + output: $output, + parallelToolCalls: $attributes['parallel_tool_calls'], + previousResponseId: $attributes['previous_response_id'], + reasoning: isset($attributes['reasoning']) + ? CreateResponseReasoning::from($attributes['reasoning']) + : null, + store: $attributes['store'], + temperature: $attributes['temperature'], + text: CreateResponseFormat::from($attributes['text']), + toolChoice: $toolChoice, + tools: $tools, + topP: $attributes['top_p'], + truncation: $attributes['truncation'], + usage: isset($attributes['usage']) + ? CreateResponseUsage::from($attributes['usage']) + : null, + user: $attributes['user'] ?? null, + metadata: $attributes['metadata'] ?? [], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + // https://github.com/phpstan/phpstan/issues/8438 + // @phpstan-ignore-next-line + return [ + 'id' => $this->id, + 'object' => $this->object, + 'created_at' => $this->createdAt, + 'status' => $this->status, + 'error' => $this->error?->toArray(), + 'incomplete_details' => $this->incompleteDetails?->toArray(), + 'instructions' => $this->instructions, + 'max_output_tokens' => $this->maxOutputTokens, + 'metadata' => $this->metadata ?? [], + 'model' => $this->model, + 'output' => array_map( + fn (OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning $output): array => $output->toArray(), + $this->output + ), + 'parallel_tool_calls' => $this->parallelToolCalls, + 'previous_response_id' => $this->previousResponseId, + 'reasoning' => $this->reasoning?->toArray(), + 'store' => $this->store, + 'temperature' => $this->temperature, + 'text' => $this->text->toArray(), + 'tool_choice' => is_string($this->toolChoice) + ? $this->toolChoice + : $this->toolChoice->toArray(), + 'tools' => array_map( + fn (ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool $tool): array => $tool->toArray(), + $this->tools + ), + 'top_p' => $this->topP, + 'truncation' => $this->truncation, + 'usage' => $this->usage?->toArray(), + 'user' => $this->user, + ]; + } +} diff --git a/src/Responses/Responses/CreateResponseError.php b/src/Responses/Responses/CreateResponseError.php new file mode 100644 index 00000000..e6125b7b --- /dev/null +++ b/src/Responses/Responses/CreateResponseError.php @@ -0,0 +1,51 @@ + + */ +final class CreateResponseError implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly string $code, + public readonly string $message + ) {} + + /** + * @param ErrorType $attributes + */ + public static function from(array $attributes): self + { + return new self( + $attributes['code'], + $attributes['message'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'code' => $this->code, + 'message' => $this->message, + ]; + } +} diff --git a/src/Responses/Responses/CreateResponseFormat.php b/src/Responses/Responses/CreateResponseFormat.php new file mode 100644 index 00000000..c00cd479 --- /dev/null +++ b/src/Responses/Responses/CreateResponseFormat.php @@ -0,0 +1,61 @@ + + */ +final class CreateResponseFormat implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly TextFormat|JsonSchemaFormat|JsonObjectFormat $format + ) {} + + /** + * @param ResponseFormatType $attributes + */ + public static function from(array $attributes): self + { + $format = match ($attributes['format']['type']) { + 'text' => TextFormat::from($attributes['format']), + 'json_schema' => JsonSchemaFormat::from($attributes['format']), + 'json_object' => JsonObjectFormat::from($attributes['format']), + }; + + return new self( + format: $format + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'format' => $this->format->toArray(), + ]; + } +} diff --git a/src/Responses/Responses/CreateResponseIncompleteDetails.php b/src/Responses/Responses/CreateResponseIncompleteDetails.php new file mode 100644 index 00000000..131fd723 --- /dev/null +++ b/src/Responses/Responses/CreateResponseIncompleteDetails.php @@ -0,0 +1,48 @@ + + */ +final class CreateResponseIncompleteDetails implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly string $reason, + ) {} + + /** + * @param IncompleteDetailsType $attributes + */ + public static function from(array $attributes): self + { + return new self( + $attributes['reason'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'reason' => $this->reason, + ]; + } +} diff --git a/src/Responses/Responses/CreateResponseReasoning.php b/src/Responses/Responses/CreateResponseReasoning.php new file mode 100644 index 00000000..cf41dbbf --- /dev/null +++ b/src/Responses/Responses/CreateResponseReasoning.php @@ -0,0 +1,51 @@ + + */ +final class CreateResponseReasoning implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly ?string $effort, + public readonly ?string $generate_summary, + ) {} + + /** + * @param ReasoningType $attributes + */ + public static function from(array $attributes): self + { + return new self( + effort: $attributes['effort'] ?? null, + generate_summary: $attributes['generate_summary'] ?? null, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'effort' => $this->effort, + 'generate_summary' => $this->generate_summary, + ]; + } +} diff --git a/src/Responses/Responses/CreateResponseUsage.php b/src/Responses/Responses/CreateResponseUsage.php new file mode 100644 index 00000000..551b2fc4 --- /dev/null +++ b/src/Responses/Responses/CreateResponseUsage.php @@ -0,0 +1,63 @@ + + */ +final class CreateResponseUsage implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly int $inputTokens, + public readonly CreateResponseUsageInputTokenDetails $inputTokensDetails, + public readonly int $outputTokens, + public readonly CreateResponseUsageOutputTokenDetails $outputTokensDetails, + public readonly int $totalTokens, + ) {} + + /** + * @param UsageType $attributes + */ + public static function from(array $attributes): self + { + return new self( + inputTokens: $attributes['input_tokens'], + inputTokensDetails: CreateResponseUsageInputTokenDetails::from($attributes['input_tokens_details']), + outputTokens: $attributes['output_tokens'], + outputTokensDetails: CreateResponseUsageOutputTokenDetails::from($attributes['output_tokens_details']), + totalTokens: $attributes['total_tokens'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'input_tokens' => $this->inputTokens, + 'input_tokens_details' => $this->inputTokensDetails->toArray(), + 'output_tokens' => $this->outputTokens, + 'output_tokens_details' => $this->outputTokensDetails->toArray(), + 'total_tokens' => $this->totalTokens, + ]; + } +} diff --git a/src/Responses/Responses/CreateResponseUsageInputTokenDetails.php b/src/Responses/Responses/CreateResponseUsageInputTokenDetails.php new file mode 100644 index 00000000..1e0114a4 --- /dev/null +++ b/src/Responses/Responses/CreateResponseUsageInputTokenDetails.php @@ -0,0 +1,48 @@ + + */ +final class CreateResponseUsageInputTokenDetails implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly int $cachedTokens, + ) {} + + /** + * @param InputTokenDetailsType $attributes + */ + public static function from(array $attributes): self + { + return new self( + $attributes['cached_tokens'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'cached_tokens' => $this->cachedTokens, + ]; + } +} diff --git a/src/Responses/Responses/CreateResponseUsageOutputTokenDetails.php b/src/Responses/Responses/CreateResponseUsageOutputTokenDetails.php new file mode 100644 index 00000000..c01a0ccf --- /dev/null +++ b/src/Responses/Responses/CreateResponseUsageOutputTokenDetails.php @@ -0,0 +1,48 @@ + + */ +final class CreateResponseUsageOutputTokenDetails implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly int $reasoningTokens, + ) {} + + /** + * @param OutputTokenDetailsType $attributes + */ + public static function from(array $attributes): self + { + return new self( + $attributes['reasoning_tokens'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'reasoning_tokens' => $this->reasoningTokens, + ]; + } +} diff --git a/src/Responses/Responses/CreateStreamedResponse.php b/src/Responses/Responses/CreateStreamedResponse.php new file mode 100644 index 00000000..b67bf045 --- /dev/null +++ b/src/Responses/Responses/CreateStreamedResponse.php @@ -0,0 +1,86 @@ +} + * + * @implements ResponseContract + */ +final class CreateStreamedResponse implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use FakeableForStreamedResponse; + + private function __construct( + public readonly string $event, + public readonly CreateResponse|OutputItem $response, + ) {} + + /** + * @param array $attributes + */ + public static function from(array $attributes): self + { + $event = $attributes['__event']; + unset($attributes['__event']); + + $meta = $attributes['__meta']; + unset($attributes['__meta']); + + $response = match ($event) { + 'response.created', + 'response.in_progress', + 'response.completed', + 'response.failed', + 'response.incomplete' => CreateResponse::from($attributes['response'], $meta), // @phpstan-ignore-line + 'response.output_item.added', + 'response.output_item.done' => OutputItem::from($attributes, $meta), // @phpstan-ignore-line + + 'response.content_part.added' => CreateResponse::from($attributes, $meta), // @phpstan-ignore-line + 'response.content_part.done' => CreateResponse::from($attributes, $meta), // @phpstan-ignore-line + 'response.output_text.delta' => CreateResponse::from($attributes, $meta), // @phpstan-ignore-line + 'response.output_text.annotation.added' => CreateResponse::from($attributes, $meta), // @phpstan-ignore-line + 'response.output_text.done' => CreateResponse::from($attributes, $meta), // @phpstan-ignore-line + 'response.refusal.delta' => CreateResponse::from($attributes, $meta), // @phpstan-ignore-line + 'response.refusal.done' => CreateResponse::from($attributes, $meta), // @phpstan-ignore-line + 'response.function_call_arguments.delta' => CreateResponse::from($attributes, $meta), // @phpstan-ignore-line + 'response.function_call_arguments.done' => CreateResponse::from($attributes, $meta), // @phpstan-ignore-line + 'response.file_search_call.in_progress' => CreateResponse::from($attributes, $meta), // @phpstan-ignore-line + 'response.file_search_call.searching' => CreateResponse::from($attributes, $meta), // @phpstan-ignore-line + 'response.file_search_call.completed' => CreateResponse::from($attributes, $meta), // @phpstan-ignore-line + 'response.web_search_call.in_progress' => CreateResponse::from($attributes, $meta), // @phpstan-ignore-line + 'response.web_search_call.searching' => CreateResponse::from($attributes, $meta), // @phpstan-ignore-line + 'response.web_search_call.completed' => CreateResponse::from($attributes, $meta), // @phpstan-ignore-line + default => throw new UnknownEventException('Unknown event: '.$event), + }; + + return new self( + $event, // @phpstan-ignore-line + $response, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'event' => $this->event, + 'data' => $this->response->toArray(), + ]; + } +} diff --git a/src/Responses/Responses/DeleteResponse.php b/src/Responses/Responses/DeleteResponse.php new file mode 100644 index 00000000..8732903f --- /dev/null +++ b/src/Responses/Responses/DeleteResponse.php @@ -0,0 +1,60 @@ + + */ +final class DeleteResponse implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + private function __construct( + public readonly string $id, + public readonly string $object, + public readonly bool $deleted, + private readonly MetaInformation $meta, + ) {} + + /** + * @param DeleteResponseType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + return new self( + id: $attributes['id'], + object: $attributes['object'], + deleted: $attributes['deleted'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'object' => $this->object, + 'deleted' => $this->deleted, + ]; + } +} diff --git a/src/Responses/Responses/Format/JsonObjectFormat.php b/src/Responses/Responses/Format/JsonObjectFormat.php new file mode 100644 index 00000000..38cb5307 --- /dev/null +++ b/src/Responses/Responses/Format/JsonObjectFormat.php @@ -0,0 +1,51 @@ + + */ +final class JsonObjectFormat implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'json_object' $type + */ + private function __construct( + public readonly string $type, + ) {} + + /** + * @param JsonObjectFormatType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Format/JsonSchemaFormat.php b/src/Responses/Responses/Format/JsonSchemaFormat.php new file mode 100644 index 00000000..3f2f22fe --- /dev/null +++ b/src/Responses/Responses/Format/JsonSchemaFormat.php @@ -0,0 +1,64 @@ +, type: 'json_schema', description: ?string, strict: ?bool} + * + * @implements ResponseContract + */ +final class JsonSchemaFormat implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $schema + * @param 'json_schema' $type + */ + private function __construct( + public readonly string $name, + public readonly array $schema, + public readonly string $type, + public readonly ?string $description, + public readonly ?bool $strict = null, + ) {} + + /** + * @param JsonSchemaFormatType $attributes + */ + public static function from(array $attributes): self + { + return new self( + name: $attributes['name'], + schema: $attributes['schema'], + type: $attributes['type'], + description: $attributes['description'] ?? null, + strict: $attributes['strict'] ?? null, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'name' => $this->name, + 'schema' => $this->schema, + 'type' => $this->type, + 'description' => $this->description, + 'strict' => $this->strict, + ]; + } +} diff --git a/src/Responses/Responses/Format/TextFormat.php b/src/Responses/Responses/Format/TextFormat.php new file mode 100644 index 00000000..8aa9e43f --- /dev/null +++ b/src/Responses/Responses/Format/TextFormat.php @@ -0,0 +1,51 @@ + + */ +final class TextFormat implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'text' $type + */ + private function __construct( + public readonly string $type, + ) {} + + /** + * @param TextFormatType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Input/AcknowledgedSafetyCheck.php b/src/Responses/Responses/Input/AcknowledgedSafetyCheck.php new file mode 100644 index 00000000..aa482752 --- /dev/null +++ b/src/Responses/Responses/Input/AcknowledgedSafetyCheck.php @@ -0,0 +1,54 @@ + + */ +final class AcknowledgedSafetyCheck implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly string $code, + public readonly string $id, + public readonly string $message, + ) {} + + /** + * @param AcknowledgedSafetyCheckType $attributes + */ + public static function from(array $attributes): self + { + return new self( + code: $attributes['code'], + id: $attributes['id'], + message: $attributes['message'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'code' => $this->code, + 'id' => $this->id, + 'message' => $this->message, + ]; + } +} diff --git a/src/Responses/Responses/Input/ComputerToolCallOutput.php b/src/Responses/Responses/Input/ComputerToolCallOutput.php new file mode 100644 index 00000000..eeb7ff2d --- /dev/null +++ b/src/Responses/Responses/Input/ComputerToolCallOutput.php @@ -0,0 +1,79 @@ +, status: 'in_progress'|'completed'|'incomplete'} + * + * @implements ResponseContract + */ +final class ComputerToolCallOutput implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'computer_call_output' $type + * @param array $acknowledgedSafetyChecks + * @param 'in_progress'|'completed'|'incomplete' $status + */ + private function __construct( + public readonly string $callId, + public readonly string $id, + public readonly ComputerToolCallOutputScreenshot $output, + public readonly string $type, + public readonly array $acknowledgedSafetyChecks, + public readonly string $status, + ) {} + + /** + * @param ComputerToolCallOutputType $attributes + */ + public static function from(array $attributes): self + { + $acknowledgedSafetyChecks = array_map( + fn (array $acknowledgedSafetyCheck) => AcknowledgedSafetyCheck::from($acknowledgedSafetyCheck), + $attributes['acknowledged_safety_checks'], + ); + + return new self( + callId: $attributes['call_id'], + id: $attributes['id'], + output: ComputerToolCallOutputScreenshot::from($attributes['output']), + type: $attributes['type'], + acknowledgedSafetyChecks: $acknowledgedSafetyChecks, + status: $attributes['status'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'call_id' => $this->callId, + 'id' => $this->id, + 'output' => $this->output->toArray(), + 'type' => $this->type, + 'acknowledged_safety_checks' => array_map( + fn (AcknowledgedSafetyCheck $acknowledgedSafetyCheck) => $acknowledgedSafetyCheck->toArray(), + $this->acknowledgedSafetyChecks, + ), + 'status' => $this->status, + ]; + } +} diff --git a/src/Responses/Responses/Input/ComputerToolCallOutputScreenshot.php b/src/Responses/Responses/Input/ComputerToolCallOutputScreenshot.php new file mode 100644 index 00000000..d56a2ccd --- /dev/null +++ b/src/Responses/Responses/Input/ComputerToolCallOutputScreenshot.php @@ -0,0 +1,57 @@ + + */ +final class ComputerToolCallOutputScreenshot implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'computer_screenshot' $type + */ + private function __construct( + public readonly string $type, + public readonly string $fileId, + public readonly string $imageUrl, + ) {} + + /** + * @param ComputerToolCallOutputScreenshotType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + fileId: $attributes['file_id'], + imageUrl: $attributes['image_url'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'file_id' => $this->fileId, + 'image_url' => $this->imageUrl, + ]; + } +} diff --git a/src/Responses/Responses/Input/FunctionToolCallOutput.php b/src/Responses/Responses/Input/FunctionToolCallOutput.php new file mode 100644 index 00000000..267e7232 --- /dev/null +++ b/src/Responses/Responses/Input/FunctionToolCallOutput.php @@ -0,0 +1,64 @@ + + */ +final class FunctionToolCallOutput implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'function_call_output' $type + * @param 'in_progress'|'completed'|'incompleted' $status + */ + private function __construct( + public readonly string $callId, + public readonly string $id, + public readonly string $output, + public readonly string $type, + public readonly string $status, + ) {} + + /** + * @param FunctionToolCallOutputType $attributes + */ + public static function from(array $attributes): self + { + return new self( + callId: $attributes['call_id'], + id: $attributes['id'], + output: $attributes['output'], + type: $attributes['type'], + status: $attributes['status'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'call_id' => $this->callId, + 'id' => $this->id, + 'output' => $this->output, + 'type' => $this->type, + 'status' => $this->status, + ]; + } +} diff --git a/src/Responses/Responses/Input/InputMessage.php b/src/Responses/Responses/Input/InputMessage.php new file mode 100644 index 00000000..e70d238d --- /dev/null +++ b/src/Responses/Responses/Input/InputMessage.php @@ -0,0 +1,81 @@ +, id: string, role: string, status: 'in_progress'|'completed'|'incomplete', type: 'message'} + * + * @implements ResponseContract + */ +final class InputMessage implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $content + * @param 'in_progress'|'completed'|'incomplete' $status + * @param 'message' $type + */ + private function __construct( + public readonly array $content, + public readonly string $id, + public readonly string $role, + public readonly string $status, + public readonly string $type, + ) {} + + /** + * @param InputMessageType $attributes + */ + public static function from(array $attributes): self + { + $content = array_map( + fn (array $item): InputMessageContentInputText|InputMessageContentInputImage|InputMessageContentInputFile => match ($item['type']) { + 'input_text' => InputMessageContentInputText::from($item), + 'input_image' => InputMessageContentInputImage::from($item), + 'input_file' => InputMessageContentInputFile::from($item), + }, + $attributes['content'], + ); + + return new self( + content: $content, + id: $attributes['id'], + role: $attributes['role'], + status: $attributes['status'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'content' => array_map( + fn (InputMessageContentInputText|InputMessageContentInputImage|InputMessageContentInputFile $item): array => $item->toArray(), + $this->content, + ), + 'id' => $this->id, + 'role' => $this->role, + 'status' => $this->status, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Input/InputMessageContentInputFile.php b/src/Responses/Responses/Input/InputMessageContentInputFile.php new file mode 100644 index 00000000..39bd985b --- /dev/null +++ b/src/Responses/Responses/Input/InputMessageContentInputFile.php @@ -0,0 +1,60 @@ + + */ +final class InputMessageContentInputFile implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'input_file' $type + */ + private function __construct( + public readonly string $type, + public readonly string $fileData, + public readonly string $fileId, + public readonly string $filename, + ) {} + + /** + * @param array{type: 'input_file', file_data: string, file_id: string, filename: string} $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + fileData: $attributes['file_data'], + fileId: $attributes['file_id'], + filename: $attributes['filename'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'file_data' => $this->fileData, + 'file_id' => $this->fileId, + 'filename' => $this->filename, + ]; + } +} diff --git a/src/Responses/Responses/Input/InputMessageContentInputImage.php b/src/Responses/Responses/Input/InputMessageContentInputImage.php new file mode 100644 index 00000000..2cdec2f6 --- /dev/null +++ b/src/Responses/Responses/Input/InputMessageContentInputImage.php @@ -0,0 +1,60 @@ + + */ +final class InputMessageContentInputImage implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'input_image' $type + */ + private function __construct( + public readonly string $type, + public readonly string $detail, + public readonly ?string $fileId, + public readonly ?string $imageUrl, + ) {} + + /** + * @param ContentInputImageType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + detail: $attributes['detail'], + fileId: $attributes['file_id'], + imageUrl: $attributes['image_url'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'detail' => $this->detail, + 'file_id' => $this->fileId, + 'image_url' => $this->imageUrl, + ]; + } +} diff --git a/src/Responses/Responses/Input/InputMessageContentInputText.php b/src/Responses/Responses/Input/InputMessageContentInputText.php new file mode 100644 index 00000000..e7934eab --- /dev/null +++ b/src/Responses/Responses/Input/InputMessageContentInputText.php @@ -0,0 +1,54 @@ + + */ +final class InputMessageContentInputText implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'input_text' $type + */ + private function __construct( + public readonly string $text, + public readonly string $type + ) {} + + /** + * @param ContentInputTextType $attributes + */ + public static function from(array $attributes): self + { + return new self( + text: $attributes['text'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'text' => $this->text, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/ListInputItems.php b/src/Responses/Responses/ListInputItems.php new file mode 100644 index 00000000..2680a9f3 --- /dev/null +++ b/src/Responses/Responses/ListInputItems.php @@ -0,0 +1,98 @@ +}>, first_id: string, last_id: string, has_more: bool}> + */ +final class ListInputItems implements ResponseContract, ResponseHasMetaInformationContract +{ + /** @use ArrayAccessible}>, first_id: string, last_id: string, has_more: bool}> */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + /** + * @param array + * }> $data + */ + private function __construct( + public readonly string $object, + public readonly array $data, + public readonly string $firstId, + public readonly string $lastId, + public readonly bool $hasMore, + private readonly MetaInformation $meta, + ) {} + + /** + * Acts as static factory, and returns a new Response instance. + * + * @param array{object: string, data: array}>, first_id: string, last_id: string, has_more: bool} $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + $data = array_map( + function (array $item): array { + $content = array_map( + fn (array $contentItem): InputMessageContentInputText|InputMessageContentInputImage|InputMessageContentInputFile => match ($contentItem['type']) { + 'input_text' => InputMessageContentInputText::from($contentItem), + 'input_image' => InputMessageContentInputImage::from($contentItem), + 'input_file' => InputMessageContentInputFile::from($contentItem), + }, + $item['content'], + ); + + return [ + 'type' => $item['type'], + 'id' => $item['id'], + 'status' => $item['status'], + 'role' => $item['role'], + 'content' => $content, + ]; + }, + $attributes['data'], + ); + + return new self( + object: $attributes['object'], + data: $data, + firstId: $attributes['first_id'], + lastId: $attributes['last_id'], + hasMore: $attributes['has_more'], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'object' => $this->object, + 'data' => $this->data, + 'first_id' => $this->firstId, + 'last_id' => $this->lastId, + 'has_more' => $this->hasMore, + ]; + } +} diff --git a/src/Responses/Responses/Output/ComputerAction/OutputComputerActionClick.php b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionClick.php new file mode 100644 index 00000000..6f2a26d0 --- /dev/null +++ b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionClick.php @@ -0,0 +1,61 @@ + + */ +final class OutputComputerActionClick implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'left'|'right'|'wheel'|'back'|'forward' $button + * @param 'click' $type + */ + private function __construct( + public readonly string $button, + public readonly string $type, + public readonly int $x, + public readonly int $y, + ) {} + + /** + * @param ClickType $attributes + */ + public static function from(array $attributes): self + { + return new self( + button: $attributes['button'], + type: $attributes['type'], + x: $attributes['x'], + y: $attributes['y'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'button' => $this->button, + 'type' => $this->type, + 'x' => $this->x, + 'y' => $this->y, + ]; + } +} diff --git a/src/Responses/Responses/Output/ComputerAction/OutputComputerActionDoubleClick.php b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionDoubleClick.php new file mode 100644 index 00000000..1706b051 --- /dev/null +++ b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionDoubleClick.php @@ -0,0 +1,57 @@ + + */ +final class OutputComputerActionDoubleClick implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'double_click' $type + */ + private function __construct( + public readonly string $type, + public readonly float $x, + public readonly float $y, + ) {} + + /** + * @param DoubleClickType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + x: $attributes['x'], + y: $attributes['y'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'x' => $this->x, + 'y' => $this->y, + ]; + } +} diff --git a/src/Responses/Responses/Output/ComputerAction/OutputComputerActionDrag.php b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionDrag.php new file mode 100644 index 00000000..7ec17910 --- /dev/null +++ b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionDrag.php @@ -0,0 +1,65 @@ +, type: 'drag'} + * + * @implements ResponseContract + */ +final class OutputComputerActionDrag implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $path + * @param 'drag' $type + */ + private function __construct( + public readonly array $path, + public readonly string $type, + ) {} + + /** + * @param DragType $attributes + */ + public static function from(array $attributes): self + { + $paths = array_map( + static fn (array $path): OutputComputerDragPath => OutputComputerDragPath::from($path), + $attributes['path'], + ); + + return new self( + path: $paths, + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'path' => array_map( + static fn (OutputComputerDragPath $path): array => $path->toArray(), + $this->path, + ), + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Output/ComputerAction/OutputComputerActionKeyPress.php b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionKeyPress.php new file mode 100644 index 00000000..a1561f45 --- /dev/null +++ b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionKeyPress.php @@ -0,0 +1,55 @@ +, type: 'keypress'} + * + * @implements ResponseContract + */ +final class OutputComputerActionKeyPress implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $keys + * @param 'keypress' $type + */ + private function __construct( + public readonly array $keys, + public readonly string $type, + ) {} + + /** + * @param KeyPressType $attributes + */ + public static function from(array $attributes): self + { + return new self( + keys: $attributes['keys'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'keys' => $this->keys, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Output/ComputerAction/OutputComputerActionMove.php b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionMove.php new file mode 100644 index 00000000..1a6fd48f --- /dev/null +++ b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionMove.php @@ -0,0 +1,57 @@ + + */ +final class OutputComputerActionMove implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'move' $type + */ + private function __construct( + public readonly string $type, + public readonly int $x, + public readonly int $y, + ) {} + + /** + * @param MoveType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + x: $attributes['x'], + y: $attributes['y'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'x' => $this->x, + 'y' => $this->y, + ]; + } +} diff --git a/src/Responses/Responses/Output/ComputerAction/OutputComputerActionScreenshot.php b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionScreenshot.php new file mode 100644 index 00000000..8138d322 --- /dev/null +++ b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionScreenshot.php @@ -0,0 +1,51 @@ + + */ +final class OutputComputerActionScreenshot implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'screenshot' $type + */ + private function __construct( + public readonly string $type, + ) {} + + /** + * @param ScreenshotType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Output/ComputerAction/OutputComputerActionScroll.php b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionScroll.php new file mode 100644 index 00000000..d4560366 --- /dev/null +++ b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionScroll.php @@ -0,0 +1,63 @@ + + */ +final class OutputComputerActionScroll implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'scroll' $type + */ + private function __construct( + public readonly int $scrollX, + public readonly int $scrollY, + public readonly string $type, + public readonly int $x, + public readonly int $y, + ) {} + + /** + * @param ScrollType $attributes + */ + public static function from(array $attributes): self + { + return new self( + scrollX: $attributes['scroll_x'], + scrollY: $attributes['scroll_y'], + type: $attributes['type'], + x: $attributes['x'], + y: $attributes['y'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'scroll_x' => $this->scrollX, + 'scroll_y' => $this->scrollY, + 'type' => $this->type, + 'x' => $this->x, + 'y' => $this->y, + ]; + } +} diff --git a/src/Responses/Responses/Output/ComputerAction/OutputComputerActionType.php b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionType.php new file mode 100644 index 00000000..a7d6d1db --- /dev/null +++ b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionType.php @@ -0,0 +1,54 @@ + + */ +final class OutputComputerActionType implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'type' $type + */ + private function __construct( + public readonly string $text, + public readonly string $type, + ) {} + + /** + * @param TypeType $attributes + */ + public static function from(array $attributes): self + { + return new self( + text: $attributes['text'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'text' => $this->text, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Output/ComputerAction/OutputComputerActionWait.php b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionWait.php new file mode 100644 index 00000000..36770c37 --- /dev/null +++ b/src/Responses/Responses/Output/ComputerAction/OutputComputerActionWait.php @@ -0,0 +1,51 @@ + + */ +final class OutputComputerActionWait implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'wait' $type + */ + private function __construct( + public readonly string $type, + ) {} + + /** + * @param WaitType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Output/ComputerAction/OutputComputerDragPath.php b/src/Responses/Responses/Output/ComputerAction/OutputComputerDragPath.php new file mode 100644 index 00000000..9d8594b9 --- /dev/null +++ b/src/Responses/Responses/Output/ComputerAction/OutputComputerDragPath.php @@ -0,0 +1,51 @@ + + */ +final class OutputComputerDragPath implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly int $x, + public readonly int $y, + ) {} + + /** + * @param DragPathType $attributes + */ + public static function from(array $attributes): self + { + return new self( + x: $attributes['x'], + y: $attributes['y'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'x' => $this->x, + 'y' => $this->y, + ]; + } +} diff --git a/src/Responses/Responses/Output/ComputerAction/OutputComputerPendingSafetyCheck.php b/src/Responses/Responses/Output/ComputerAction/OutputComputerPendingSafetyCheck.php new file mode 100644 index 00000000..13d2b178 --- /dev/null +++ b/src/Responses/Responses/Output/ComputerAction/OutputComputerPendingSafetyCheck.php @@ -0,0 +1,54 @@ + + */ +final class OutputComputerPendingSafetyCheck implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly string $code, + public readonly string $id, + public readonly string $message, + ) {} + + /** + * @param PendingSafetyCheckType $attributes + */ + public static function from(array $attributes): self + { + return new self( + code: $attributes['code'], + id: $attributes['id'], + message: $attributes['message'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'code' => $this->code, + 'id' => $this->id, + 'message' => $this->message, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputComputerToolCall.php b/src/Responses/Responses/Output/OutputComputerToolCall.php new file mode 100644 index 00000000..cb598e53 --- /dev/null +++ b/src/Responses/Responses/Output/OutputComputerToolCall.php @@ -0,0 +1,109 @@ +, status: 'in_progress'|'completed'|'incomplete', type: 'computer_call'} + * + * @implements ResponseContract + */ +final class OutputComputerToolCall implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $pendingSafetyChecks + * @param 'in_progress'|'completed'|'incomplete' $status + * @param 'computer_call' $type + */ + private function __construct( + public readonly Click|DoubleClick|Drag|KeyPress|Move|Screenshot|Scroll|Type|Wait $action, + public readonly string $callId, + public readonly string $id, + public readonly array $pendingSafetyChecks, + public readonly string $status, + public readonly string $type, + ) {} + + /** + * @param OutputComputerToolCallType $attributes + */ + public static function from(array $attributes): self + { + $action = match ($attributes['action']['type']) { + 'click' => Click::from($attributes['action']), + 'double_click' => DoubleClick::from($attributes['action']), + 'drag' => Drag::from($attributes['action']), + 'keypress' => KeyPress::from($attributes['action']), + 'move' => Move::from($attributes['action']), + 'screenshot' => Screenshot::from($attributes['action']), + 'scroll' => Scroll::from($attributes['action']), + 'type' => Type::from($attributes['action']), + 'wait' => Wait::from($attributes['action']), + }; + + $pendingSafetyChecks = array_map( + fn (array $safetyCheck): OutputComputerPendingSafetyCheck => OutputComputerPendingSafetyCheck::from($safetyCheck), + $attributes['pending_safety_checks'] + ); + + return new self( + action: $action, + callId: $attributes['call_id'], + id: $attributes['id'], + pendingSafetyChecks: $pendingSafetyChecks, + status: $attributes['status'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'call_id' => $this->callId, + 'id' => $this->id, + 'action' => $this->action->toArray(), + 'pending_safety_checks' => array_map( + fn (OutputComputerPendingSafetyCheck $safetyCheck): array => $safetyCheck->toArray(), + $this->pendingSafetyChecks, + ), + 'status' => $this->status, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputFileSearchToolCall.php b/src/Responses/Responses/Output/OutputFileSearchToolCall.php new file mode 100644 index 00000000..961d9565 --- /dev/null +++ b/src/Responses/Responses/Output/OutputFileSearchToolCall.php @@ -0,0 +1,78 @@ +, status: 'in_progress'|'searching'|'incomplete'|'failed', type: 'file_search_call', results: ?array} + * + * @implements ResponseContract + */ +final class OutputFileSearchToolCall implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $queries + * @param 'in_progress'|'searching'|'incomplete'|'failed' $status + * @param 'file_search_call' $type + * @param ?array $results + */ + private function __construct( + public readonly string $id, + public readonly array $queries, + public readonly string $status, + public readonly string $type, + public readonly ?array $results = null, + ) {} + + /** + * @param OutputFileSearchToolCallType $attributes + */ + public static function from(array $attributes): self + { + $results = isset($attributes['results']) + ? array_map( + fn (array $result): OutputFileSearchToolCallResult => OutputFileSearchToolCallResult::from($result), + $attributes['results'] + ) + : null; + + return new self( + id: $attributes['id'], + queries: $attributes['queries'], + status: $attributes['status'], + type: $attributes['type'], + results: $results, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'queries' => $this->queries, + 'status' => $this->status, + 'type' => $this->type, + 'results' => isset($this->results) ? array_map( + fn (OutputFileSearchToolCallResult $result) => $result->toArray(), + $this->results + ) : null, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputFileSearchToolCallResult.php b/src/Responses/Responses/Output/OutputFileSearchToolCallResult.php new file mode 100644 index 00000000..7576c01a --- /dev/null +++ b/src/Responses/Responses/Output/OutputFileSearchToolCallResult.php @@ -0,0 +1,63 @@ +, file_id: string, filename: string, score: float, text: string} + * + * @implements ResponseContract + */ +final class OutputFileSearchToolCallResult implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $attributes + */ + private function __construct( + public readonly array $attributes, + public readonly string $fileId, + public readonly string $filename, + public readonly float $score, + public readonly string $text, + ) {} + + /** + * @param OutputFileSearchToolCallResultType $attributes + */ + public static function from(array $attributes): self + { + return new self( + attributes: $attributes['attributes'], + fileId: $attributes['file_id'], + filename: $attributes['filename'], + score: $attributes['score'], + text: $attributes['text'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'attributes' => $this->attributes, + 'file_id' => $this->fileId, + 'filename' => $this->filename, + 'score' => $this->score, + 'text' => $this->text, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputFunctionToolCall.php b/src/Responses/Responses/Output/OutputFunctionToolCall.php new file mode 100644 index 00000000..985fa9d2 --- /dev/null +++ b/src/Responses/Responses/Output/OutputFunctionToolCall.php @@ -0,0 +1,67 @@ + + */ +final class OutputFunctionToolCall implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'function_call' $type + * @param 'in_progress'|'completed'|'incomplete' $status + */ + private function __construct( + public readonly string $arguments, + public readonly string $callId, + public readonly string $name, + public readonly string $type, + public readonly string $id, + public readonly string $status, + ) {} + + /** + * @param OutputFunctionToolCallType $attributes + */ + public static function from(array $attributes): self + { + return new self( + arguments: $attributes['arguments'], + callId: $attributes['call_id'], + name: $attributes['name'], + type: $attributes['type'], + id: $attributes['id'], + status: $attributes['status'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'arguments' => $this->arguments, + 'call_id' => $this->callId, + 'name' => $this->name, + 'type' => $this->type, + 'id' => $this->id, + 'status' => $this->status, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputMessage.php b/src/Responses/Responses/Output/OutputMessage.php new file mode 100644 index 00000000..dfa957e2 --- /dev/null +++ b/src/Responses/Responses/Output/OutputMessage.php @@ -0,0 +1,80 @@ +, id: string, role: 'assistant', status: 'in_progress'|'completed'|'incomplete', type: 'message'} + * + * @implements ResponseContract + */ +final class OutputMessage implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $content + * @param 'assistant' $role + * @param 'in_progress'|'completed'|'incomplete' $status + * @param 'message' $type + */ + private function __construct( + public readonly array $content, + public readonly string $id, + public readonly string $role, + public readonly string $status, + public readonly string $type, + ) {} + + /** + * @param OutputMessageType $attributes + */ + public static function from(array $attributes): self + { + $content = array_map( + fn (array $item): OutputMessageContentOutputText|OutputMessageContentRefusal => match ($item['type']) { + 'output_text' => OutputMessageContentOutputText::from($item), + 'refusal' => OutputMessageContentRefusal::from($item), + }, + $attributes['content'], + ); + + return new self( + content: $content, + id: $attributes['id'], + role: $attributes['role'], + status: $attributes['status'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'content' => array_map( + fn (OutputMessageContentOutputText|OutputMessageContentRefusal $item): array => $item->toArray(), + $this->content, + ), + 'id' => $this->id, + 'role' => $this->role, + 'status' => $this->status, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputMessageContentOutputText.php b/src/Responses/Responses/Output/OutputMessageContentOutputText.php new file mode 100644 index 00000000..77eedbfe --- /dev/null +++ b/src/Responses/Responses/Output/OutputMessageContentOutputText.php @@ -0,0 +1,77 @@ +, text: string, type: 'output_text'} + * + * @implements ResponseContract + */ +final class OutputMessageContentOutputText implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $annotations + * @param 'output_text' $type + */ + private function __construct( + public readonly array $annotations, + public readonly string $text, + public readonly string $type + ) {} + + /** + * @param OutputTextType $attributes + */ + 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), + }, + $attributes['annotations'], + ); + + return new self( + annotations: $annotations, + text: $attributes['text'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'annotations' => array_map( + fn (AnnotationFileCitation|AnnotationFilePath|AnnotationUrlCitation $annotation): array => $annotation->toArray(), + $this->annotations, + ), + 'text' => $this->text, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsFileCitation.php b/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsFileCitation.php new file mode 100644 index 00000000..073e3f35 --- /dev/null +++ b/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsFileCitation.php @@ -0,0 +1,57 @@ + + */ +final class OutputMessageContentOutputTextAnnotationsFileCitation implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'file_citation' $type + */ + private function __construct( + public readonly string $fileId, + public readonly int $index, + public readonly string $type, + ) {} + + /** + * @param array{file_id: string, index: int, type: 'file_citation'} $attributes + */ + public static function from(array $attributes): self + { + return new self( + fileId: $attributes['file_id'], + index: $attributes['index'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'file_id' => $this->fileId, + 'index' => $this->index, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsFilePath.php b/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsFilePath.php new file mode 100644 index 00000000..11272c46 --- /dev/null +++ b/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsFilePath.php @@ -0,0 +1,57 @@ + + */ +final class OutputMessageContentOutputTextAnnotationsFilePath implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'file_path' $type + */ + private function __construct( + public readonly string $fileId, + public readonly int $index, + public readonly string $type, + ) {} + + /** + * @param FilePathType $attributes + */ + public static function from(array $attributes): self + { + return new self( + fileId: $attributes['file_id'], + index: $attributes['index'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'file_id' => $this->fileId, + 'index' => $this->index, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsUrlCitation.php b/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsUrlCitation.php new file mode 100644 index 00000000..469fd2de --- /dev/null +++ b/src/Responses/Responses/Output/OutputMessageContentOutputTextAnnotationsUrlCitation.php @@ -0,0 +1,63 @@ + + */ +final class OutputMessageContentOutputTextAnnotationsUrlCitation implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'url_citation' $type + */ + private function __construct( + public readonly int $endIndex, + public readonly int $startIndex, + public readonly string $title, + public readonly string $type, + public readonly string $url, + ) {} + + /** + * @param UrlCitationType $attributes + */ + public static function from(array $attributes): self + { + return new self( + endIndex: $attributes['end_index'], + startIndex: $attributes['start_index'], + title: $attributes['title'], + type: $attributes['type'], + url: $attributes['url'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'end_index' => $this->endIndex, + 'start_index' => $this->startIndex, + 'title' => $this->title, + 'type' => $this->type, + 'url' => $this->url, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputMessageContentRefusal.php b/src/Responses/Responses/Output/OutputMessageContentRefusal.php new file mode 100644 index 00000000..b19d72c0 --- /dev/null +++ b/src/Responses/Responses/Output/OutputMessageContentRefusal.php @@ -0,0 +1,54 @@ + + */ +final class OutputMessageContentRefusal implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'refusal' $type + */ + private function __construct( + public readonly string $refusal, + public readonly string $type, + ) {} + + /** + * @param ContentRefusalType $attributes + */ + public static function from(array $attributes): self + { + return new self( + refusal: $attributes['refusal'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'refusal' => $this->refusal, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputReasoning.php b/src/Responses/Responses/Output/OutputReasoning.php new file mode 100644 index 00000000..983f089a --- /dev/null +++ b/src/Responses/Responses/Output/OutputReasoning.php @@ -0,0 +1,72 @@ +, type: 'reasoning', status: 'in_progress'|'completed'|'incomplete'} + * + * @implements ResponseContract + */ +final class OutputReasoning implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $summary + * @param 'reasoning' $type + * @param 'in_progress'|'completed'|'incomplete' $status + */ + private function __construct( + public readonly string $id, + public readonly array $summary, + public readonly string $type, + public readonly string $status, + ) {} + + /** + * @param OutputReasoningType $attributes + */ + public static function from(array $attributes): self + { + $summary = array_map( + static fn (array $summary): OutputReasoningSummary => OutputReasoningSummary::from($summary), + $attributes['summary'], + ); + + return new self( + id: $attributes['id'], + summary: $summary, + type: $attributes['type'], + status: $attributes['status'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'summary' => array_map( + static fn (OutputReasoningSummary $summary): array => $summary->toArray(), + $this->summary, + ), + 'type' => $this->type, + 'status' => $this->status, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputReasoningSummary.php b/src/Responses/Responses/Output/OutputReasoningSummary.php new file mode 100644 index 00000000..5c80ed81 --- /dev/null +++ b/src/Responses/Responses/Output/OutputReasoningSummary.php @@ -0,0 +1,54 @@ + + */ +final class OutputReasoningSummary implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'summary_text' $type + */ + private function __construct( + public readonly string $text, + public readonly string $type, + ) {} + + /** + * @param ReasoningSummaryType $attributes + */ + public static function from(array $attributes): self + { + return new self( + text: $attributes['text'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'text' => $this->text, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Output/OutputWebSearchToolCall.php b/src/Responses/Responses/Output/OutputWebSearchToolCall.php new file mode 100644 index 00000000..1fe2fadc --- /dev/null +++ b/src/Responses/Responses/Output/OutputWebSearchToolCall.php @@ -0,0 +1,57 @@ + + */ +final class OutputWebSearchToolCall implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'web_search_call' $type + */ + private function __construct( + public readonly string $id, + public readonly string $status, + public readonly string $type, + ) {} + + /** + * @param OutputWebSearchToolCallType $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/RetrieveResponse.php b/src/Responses/Responses/RetrieveResponse.php new file mode 100644 index 00000000..35902b80 --- /dev/null +++ b/src/Responses/Responses/RetrieveResponse.php @@ -0,0 +1,208 @@ + + * @phpstan-type OutputType array + * @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|null} + * + * @implements ResponseContract + */ +final class RetrieveResponse implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + /** + * @param 'response' $object + * @param 'completed'|'failed'|'in_progress'|'incomplete' $status + * @param array $output + * @param array $tools + * @param 'auto'|'disabled'|null $truncation + * @param array $metadata + */ + private function __construct( + public readonly string $id, + public readonly string $object, + public readonly int $createdAt, + public readonly string $status, + public readonly ?CreateResponseError $error, + public readonly ?CreateResponseIncompleteDetails $incompleteDetails, + public readonly ?string $instructions, + public readonly ?int $maxOutputTokens, + public readonly string $model, + public readonly array $output, + public readonly bool $parallelToolCalls, + public readonly ?string $previousResponseId, + public readonly ?CreateResponseReasoning $reasoning, + public readonly bool $store, + public readonly ?float $temperature, + public readonly CreateResponseFormat $text, + public readonly string|FunctionToolChoice|HostedToolChoice $toolChoice, + public readonly array $tools, + public readonly ?float $topP, + public readonly ?string $truncation, + public readonly ?CreateResponseUsage $usage, + public readonly ?string $user, + public array $metadata, + private readonly MetaInformation $meta, + ) {} + + /** + * @param RetrieveResponseType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + $output = array_map( + fn (array $output): OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning => match ($output['type']) { + 'message' => OutputMessage::from($output), + 'file_search_call' => OutputFileSearchToolCall::from($output), + 'function_call' => OutputFunctionToolCall::from($output), + 'web_search_call' => OutputWebSearchToolCall::from($output), + 'computer_call' => OutputComputerToolCall::from($output), + 'reasoning' => OutputReasoning::from($output), + }, + $attributes['output'], + ); + + $toolChoice = is_array($attributes['tool_choice']) + ? match ($attributes['tool_choice']['type']) { + 'file_search', 'web_search_preview', 'computer_use_preview' => HostedToolChoice::from($attributes['tool_choice']), + 'function' => FunctionToolChoice::from($attributes['tool_choice']), + } + : $attributes['tool_choice']; + + $tools = array_map( + fn (array $tool): ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool => 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), + }, + $attributes['tools'], + ); + + return new self( + id: $attributes['id'], + object: $attributes['object'], + createdAt: $attributes['created_at'], + status: $attributes['status'], + error: isset($attributes['error']) + ? CreateResponseError::from($attributes['error']) + : null, + incompleteDetails: isset($attributes['incomplete_details']) + ? CreateResponseIncompleteDetails::from($attributes['incomplete_details']) + : null, + instructions: $attributes['instructions'], + maxOutputTokens: $attributes['max_output_tokens'], + model: $attributes['model'], + output: $output, + parallelToolCalls: $attributes['parallel_tool_calls'], + previousResponseId: $attributes['previous_response_id'], + reasoning: isset($attributes['reasoning']) + ? CreateResponseReasoning::from($attributes['reasoning']) + : null, + store: $attributes['store'], + temperature: $attributes['temperature'], + text: CreateResponseFormat::from($attributes['text']), + toolChoice: $toolChoice, + tools: $tools, + topP: $attributes['top_p'], + truncation: $attributes['truncation'], + usage: isset($attributes['usage']) + ? CreateResponseUsage::from($attributes['usage']) + : null, + user: $attributes['user'] ?? null, + metadata: $attributes['metadata'] ?? [], + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + // https://github.com/phpstan/phpstan/issues/8438 + // @phpstan-ignore-next-line + return [ + 'id' => $this->id, + 'object' => $this->object, + 'created_at' => $this->createdAt, + 'status' => $this->status, + 'error' => $this->error?->toArray(), + 'incomplete_details' => $this->incompleteDetails?->toArray(), + 'instructions' => $this->instructions, + 'max_output_tokens' => $this->maxOutputTokens, + 'metadata' => $this->metadata ?? [], + 'model' => $this->model, + 'output' => array_map( + fn (OutputMessage|OutputComputerToolCall|OutputFileSearchToolCall|OutputWebSearchToolCall|OutputFunctionToolCall|OutputReasoning $output): array => $output->toArray(), + $this->output + ), + 'parallel_tool_calls' => $this->parallelToolCalls, + 'previous_response_id' => $this->previousResponseId, + 'reasoning' => $this->reasoning?->toArray(), + 'store' => $this->store, + 'temperature' => $this->temperature, + 'text' => $this->text->toArray(), + 'tool_choice' => is_string($this->toolChoice) + ? $this->toolChoice + : $this->toolChoice->toArray(), + 'tools' => array_map( + fn (ComputerUseTool|FileSearchTool|FunctionTool|WebSearchTool $tool): array => $tool->toArray(), + $this->tools + ), + 'top_p' => $this->topP, + 'truncation' => $this->truncation, + 'usage' => $this->usage?->toArray(), + 'user' => $this->user, + ]; + } +} diff --git a/src/Responses/Responses/Streaming/OutputItem.php b/src/Responses/Responses/Streaming/OutputItem.php new file mode 100644 index 00000000..d1324a7b --- /dev/null +++ b/src/Responses/Responses/Streaming/OutputItem.php @@ -0,0 +1,79 @@ + + */ +final class OutputItem implements ResponseContract, ResponseHasMetaInformationContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + use HasMetaInformation; + + private function __construct( + public readonly int $outputIndex, + public readonly OutputMessage|OutputFileSearchToolCall|OutputFunctionToolCall|OutputWebSearchToolCall|OutputComputerToolCall|OutputReasoning $item, + private readonly MetaInformation $meta, + ) {} + + /** + * @param OutputItemType $attributes + */ + public static function from(array $attributes, MetaInformation $meta): self + { + $item = match ($attributes['item']['type']) { + 'message' => OutputMessage::from($attributes['item']), + 'file_search_call' => OutputFileSearchToolCall::from($attributes['item']), + 'function_call' => OutputFunctionToolCall::from($attributes['item']), + 'web_search_call' => OutputWebSearchToolCall::from($attributes['item']), + 'computer_call' => OutputComputerToolCall::from($attributes['item']), + 'reasoning' => OutputReasoning::from($attributes['item']), + }; + + return new self( + outputIndex: $attributes['output_index'], + item: $item, + meta: $meta, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'output_index' => $this->outputIndex, + 'item' => $this->item->toArray(), + ]; + } +} diff --git a/src/Responses/Responses/Tool/ComputerUseTool.php b/src/Responses/Responses/Tool/ComputerUseTool.php new file mode 100644 index 00000000..6839312e --- /dev/null +++ b/src/Responses/Responses/Tool/ComputerUseTool.php @@ -0,0 +1,60 @@ + + */ +final class ComputerUseTool implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'computer_use_preview' $type + */ + private function __construct( + public readonly int $displayHeight, + public readonly int $displayWidth, + public readonly string $environment, + public readonly string $type, + ) {} + + /** + * @param ComputerUseToolType $attributes + */ + public static function from(array $attributes): self + { + return new self( + displayHeight: $attributes['display_height'], + displayWidth: $attributes['display_width'], + environment: $attributes['environment'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'display_height' => $this->displayHeight, + 'display_width' => $this->displayWidth, + 'environment' => $this->environment, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Tool/FileSearchComparisonFilter.php b/src/Responses/Responses/Tool/FileSearchComparisonFilter.php new file mode 100644 index 00000000..e0a6f67a --- /dev/null +++ b/src/Responses/Responses/Tool/FileSearchComparisonFilter.php @@ -0,0 +1,57 @@ + + */ +final class FileSearchComparisonFilter implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'eq'|'ne'|'gt'|'gte'|'lt'|'lte' $type + */ + private function __construct( + public readonly string $key, + public readonly string $type, + public readonly string|int|bool $value, + ) {} + + /** + * @param ComparisonFilterType $attributes + */ + public static function from(array $attributes): self + { + return new self( + key: $attributes['key'], + type: $attributes['type'], + value: $attributes['value'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'key' => $this->key, + 'type' => $this->type, + 'value' => $this->value, + ]; + } +} diff --git a/src/Responses/Responses/Tool/FileSearchCompoundFilter.php b/src/Responses/Responses/Tool/FileSearchCompoundFilter.php new file mode 100644 index 00000000..5eacabec --- /dev/null +++ b/src/Responses/Responses/Tool/FileSearchCompoundFilter.php @@ -0,0 +1,65 @@ +, type: 'and'|'or'} + * + * @implements ResponseContract + */ +final class FileSearchCompoundFilter implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $filters + * @param 'and'|'or' $type + */ + private function __construct( + public readonly array $filters, + public readonly string $type, + ) {} + + /** + * @param CompoundFilterType $attributes + */ + public static function from(array $attributes): self + { + $filters = array_map( + static fn (array $filter): FileSearchComparisonFilter => FileSearchComparisonFilter::from($filter), + $attributes['filters'], + ); + + return new self( + filters: $filters, + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'filters' => array_map( + static fn (FileSearchComparisonFilter $filter): array => $filter->toArray(), + $this->filters, + ), + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/Tool/FileSearchRankingOption.php b/src/Responses/Responses/Tool/FileSearchRankingOption.php new file mode 100644 index 00000000..20cf72f3 --- /dev/null +++ b/src/Responses/Responses/Tool/FileSearchRankingOption.php @@ -0,0 +1,51 @@ + + */ +final class FileSearchRankingOption implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + private function __construct( + public readonly string $ranker, + public readonly float $scoreThreshold, + ) {} + + /** + * @param RankingOptionType $attributes + */ + public static function from(array $attributes): self + { + return new self( + ranker: $attributes['ranker'], + scoreThreshold: $attributes['score_threshold'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'ranker' => $this->ranker, + 'score_threshold' => $this->scoreThreshold, + ]; + } +} diff --git a/src/Responses/Responses/Tool/FileSearchTool.php b/src/Responses/Responses/Tool/FileSearchTool.php new file mode 100644 index 00000000..61742832 --- /dev/null +++ b/src/Responses/Responses/Tool/FileSearchTool.php @@ -0,0 +1,73 @@ +, filters: ComparisonFilterType|CompoundFilterType, max_num_results: int, ranking_options: RankingOptionType} + * + * @implements ResponseContract + */ +final class FileSearchTool implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $vectorStoreIds + * @param 'file_search' $type + */ + private function __construct( + public readonly string $type, + public readonly array $vectorStoreIds, + public readonly FileSearchComparisonFilter|FileSearchCompoundFilter $filters, + public readonly int $maxNumResults, + public readonly FileSearchRankingOption $rankingOptions, + ) {} + + /** + * @param FileSearchToolType $attributes + */ + public static function from(array $attributes): self + { + $filters = match ($attributes['filters']['type']) { + 'eq', 'ne', 'gt', 'gte', 'lt', 'lte' => FileSearchComparisonFilter::from($attributes['filters']), + 'and', 'or' => FileSearchCompoundFilter::from($attributes['filters']), + }; + + return new self( + type: $attributes['type'], + vectorStoreIds: $attributes['vector_store_ids'], + filters: $filters, + maxNumResults: $attributes['max_num_results'], + rankingOptions: FileSearchRankingOption::from($attributes['ranking_options']), + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'vector_store_ids' => $this->vectorStoreIds, + 'filters' => $this->filters->toArray(), + 'max_num_results' => $this->maxNumResults, + 'ranking_options' => $this->rankingOptions->toArray(), + ]; + } +} diff --git a/src/Responses/Responses/Tool/FunctionTool.php b/src/Responses/Responses/Tool/FunctionTool.php new file mode 100644 index 00000000..b7b6e86d --- /dev/null +++ b/src/Responses/Responses/Tool/FunctionTool.php @@ -0,0 +1,64 @@ +, strict: bool, type: 'function', description: ?string} + * + * @implements ResponseContract + */ +final class FunctionTool implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param array $parameters + * @param 'function' $type + */ + private function __construct( + public readonly string $name, + public readonly array $parameters, + public readonly bool $strict, + public readonly string $type, + public readonly ?string $description = null, + ) {} + + /** + * @param FunctionToolType $attributes + */ + public static function from(array $attributes): self + { + return new self( + name: $attributes['name'], + parameters: $attributes['parameters'], + strict: $attributes['strict'], + type: $attributes['type'], + description: $attributes['description'] ?? null, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'name' => $this->name, + 'parameters' => $this->parameters, + 'strict' => $this->strict, + 'type' => $this->type, + 'description' => $this->description, + ]; + } +} diff --git a/src/Responses/Responses/Tool/WebSearchTool.php b/src/Responses/Responses/Tool/WebSearchTool.php new file mode 100644 index 00000000..c93cae06 --- /dev/null +++ b/src/Responses/Responses/Tool/WebSearchTool.php @@ -0,0 +1,62 @@ + + */ +final class WebSearchTool implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'web_search_preview'|'web_search_preview_2025_03_11' $type + * @param 'low'|'medium'|'high' $searchContextSize + */ + private function __construct( + public readonly string $type, + public readonly string $searchContextSize, + public readonly ?WebSearchUserLocation $userLocation, + ) {} + + /** + * @param WebSearchToolType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + searchContextSize: $attributes['search_context_size'], + userLocation: isset($attributes['user_location']) + ? WebSearchUserLocation::from($attributes['user_location']) + : null, + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'search_context_size' => $this->searchContextSize, + 'user_location' => $this->userLocation?->toArray(), + ]; + } +} diff --git a/src/Responses/Responses/Tool/WebSearchUserLocation.php b/src/Responses/Responses/Tool/WebSearchUserLocation.php new file mode 100644 index 00000000..601726ef --- /dev/null +++ b/src/Responses/Responses/Tool/WebSearchUserLocation.php @@ -0,0 +1,63 @@ + + */ +final class WebSearchUserLocation implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'approximate' $type + */ + private function __construct( + public readonly string $type, + public readonly ?string $city, + public readonly string $country, + public readonly ?string $region, + public readonly ?string $timezone, + ) {} + + /** + * @param UserLocationType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + city: $attributes['city'], + country: $attributes['country'], + region: $attributes['region'], + timezone: $attributes['timezone'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + 'city' => $this->city, + 'country' => $this->country, + 'region' => $this->region, + 'timezone' => $this->timezone, + ]; + } +} diff --git a/src/Responses/Responses/ToolChoice/FunctionToolChoice.php b/src/Responses/Responses/ToolChoice/FunctionToolChoice.php new file mode 100644 index 00000000..8faf2571 --- /dev/null +++ b/src/Responses/Responses/ToolChoice/FunctionToolChoice.php @@ -0,0 +1,54 @@ + + */ +final class FunctionToolChoice implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'function' $type + */ + private function __construct( + public readonly string $name, + public readonly string $type, + ) {} + + /** + * @param FunctionToolChoiceType $attributes + */ + public static function from(array $attributes): self + { + return new self( + name: $attributes['name'], + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'name' => $this->name, + 'type' => $this->type, + ]; + } +} diff --git a/src/Responses/Responses/ToolChoice/HostedToolChoice.php b/src/Responses/Responses/ToolChoice/HostedToolChoice.php new file mode 100644 index 00000000..3b012c13 --- /dev/null +++ b/src/Responses/Responses/ToolChoice/HostedToolChoice.php @@ -0,0 +1,51 @@ + + */ +final class HostedToolChoice implements ResponseContract +{ + /** + * @use ArrayAccessible + */ + use ArrayAccessible; + + use Fakeable; + + /** + * @param 'file_search'|'web_search_preview'|'computer_use_preview' $type + */ + private function __construct( + public readonly string $type, + ) {} + + /** + * @param HostedToolChoiceType $attributes + */ + public static function from(array $attributes): self + { + return new self( + type: $attributes['type'], + ); + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return [ + 'type' => $this->type, + ]; + } +} diff --git a/src/Testing/ClientFake.php b/src/Testing/ClientFake.php index 828f2a28..db57bb49 100644 --- a/src/Testing/ClientFake.php +++ b/src/Testing/ClientFake.php @@ -21,6 +21,7 @@ use OpenAI\Testing\Resources\ImagesTestResource; use OpenAI\Testing\Resources\ModelsTestResource; use OpenAI\Testing\Resources\ModerationsTestResource; +use OpenAI\Testing\Resources\ResponsesTestResource; use OpenAI\Testing\Resources\ThreadsTestResource; use OpenAI\Testing\Resources\VectorStoresTestResource; use PHPUnit\Framework\Assert as PHPUnit; @@ -132,6 +133,11 @@ public function record(TestRequest $request): ResponseContract|ResponseStreamCon return $response; } + public function responses(): ResponsesTestResource + { + return new ResponsesTestResource($this); + } + public function completions(): CompletionsTestResource { return new CompletionsTestResource($this); diff --git a/src/Testing/Resources/ResponsesTestResource.php b/src/Testing/Resources/ResponsesTestResource.php new file mode 100644 index 00000000..7468b3a8 --- /dev/null +++ b/src/Testing/Resources/ResponsesTestResource.php @@ -0,0 +1,47 @@ +record(__FUNCTION__, func_get_args()); + } + + public function createStreamed(array $parameters): StreamResponse + { + return $this->record(__FUNCTION__, func_get_args()); + } + + public function retrieve(string $id): RetrieveResponse + { + return $this->record(__FUNCTION__, func_get_args()); + } + + public function list(string $id, array $parameters = []): ListInputItems + { + return $this->record(__FUNCTION__, func_get_args()); + } + + public function delete(string $id): DeleteResponse + { + return $this->record(__FUNCTION__, func_get_args()); + } +} diff --git a/src/Testing/Responses/Concerns/Fakeable.php b/src/Testing/Responses/Concerns/Fakeable.php index da0b117f..ca55e3ef 100644 --- a/src/Testing/Responses/Concerns/Fakeable.php +++ b/src/Testing/Responses/Concerns/Fakeable.php @@ -13,7 +13,7 @@ trait Fakeable */ public static function fake(array $override = [], ?MetaInformation $meta = null): static { - $class = str_replace('Responses\\', 'Testing\\Responses\\Fixtures\\', static::class).'Fixture'; + $class = str_replace('OpenAI\\Responses\\', 'OpenAI\\Testing\\Responses\\Fixtures\\', static::class).'Fixture'; return static::from( self::buildAttributes($class::ATTRIBUTES, $override), diff --git a/src/Testing/Responses/Fixtures/Responses/CreateResponseFixture.php b/src/Testing/Responses/Fixtures/Responses/CreateResponseFixture.php new file mode 100644 index 00000000..1d16c224 --- /dev/null +++ b/src/Testing/Responses/Fixtures/Responses/CreateResponseFixture.php @@ -0,0 +1,103 @@ + 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', + 'object' => 'response', + 'created_at' => 1741484430, + 'status' => 'completed', + 'error' => null, + 'incomplete_details' => null, + 'instructions' => null, + 'max_output_tokens' => null, + 'model' => 'gpt-4o-2024-08-06', + 'output' => [ + [ + 'type' => 'web_search_call', + 'id' => 'ws_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'status' => 'completed', + ], + [ + 'type' => 'message', + 'id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'status' => 'completed', + 'role' => 'assistant', + 'content' => [ + [ + 'type' => 'output_text', + 'text' => 'As of today, March 9, 2025, one notable positive news story...', + 'annotations' => [ + [ + 'type' => 'url_citation', + 'start_index' => 442, + 'end_index' => 557, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + [ + 'type' => 'url_citation', + 'start_index' => 962, + 'end_index' => 1077, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + [ + 'type' => 'url_citation', + 'start_index' => 1336, + 'end_index' => 1451, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + ], + ], + ], + ], + ], + 'parallel_tool_calls' => true, + 'previous_response_id' => null, + 'reasoning' => [ + 'effort' => null, + 'generate_summary' => null, + ], + 'store' => true, + 'temperature' => 1.0, + 'text' => [ + 'format' => [ + 'type' => 'text', + ], + ], + 'tool_choice' => 'auto', + 'tools' => [ + [ + 'type' => 'web_search_preview', + 'domains' => [], + 'search_context_size' => 'medium', + 'user_location' => [ + 'type' => 'approximate', + 'city' => 'San Francisco', + 'country' => 'US', + 'region' => 'California', + 'timezone' => 'America/Los_Angeles', + ], + ], + ], + 'top_p' => 1.0, + 'truncation' => 'disabled', + 'usage' => [ + 'input_tokens' => 328, + 'input_tokens_details' => [ + 'cached_tokens' => 0, + ], + 'output_tokens' => 356, + 'output_tokens_details' => [ + 'reasoning_tokens' => 0, + ], + 'total_tokens' => 684, + ], + 'user' => null, + 'metadata' => [], + ]; +} diff --git a/src/Testing/Responses/Fixtures/Responses/CreateStreamedResponseFixture.txt b/src/Testing/Responses/Fixtures/Responses/CreateStreamedResponseFixture.txt new file mode 100644 index 00000000..b7b9311e --- /dev/null +++ b/src/Testing/Responses/Fixtures/Responses/CreateStreamedResponseFixture.txt @@ -0,0 +1,9 @@ +data: {"type":"response.created","response":{"id":"resp_67c9fdcecf488190bdd9a0409de3a1ec07b8b0ad4e5eb654","object":"response","created_at":1741290958,"status":"in_progress","error":null,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} +data: {"type":"response.in_progress","response":{"id":"resp_67c9fdcecf488190bdd9a0409de3a1ec07b8b0ad4e5eb654","object":"response","created_at":1741290958,"status":"in_progress","error":null,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} +data: {"type":"response.output_item.added","output_index":0,"item":{"id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","type":"message","status":"in_progress","role":"assistant","content":[]}} +data: {"type":"response.content_part.added","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]}} +data: {"type":"response.output_text.delta","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"delta":"Hi"} +data: {"type":"response.output_text.done","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"text":"Hi there! How can I assist you today?"} +data: {"type":"response.content_part.done","item_id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","output_index":0,"content_index":0,"part":{"type":"output_text","text":"Hi there! How can I assist you today?","annotations":[]}} +data: {"type":"response.output_item.done","output_index":0,"item":{"id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Hi there! How can I assist you today?","annotations":[]}]}} +data: {"type":"response.completed","response":{"id":"resp_67c9fdcecf488190bdd9a0409de3a1ec07b8b0ad4e5eb654","object":"response","created_at":1741290958,"status":"completed","error":null,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[{"id":"msg_67c9fdcf37fc8190ba82116e33fb28c507b8b0ad4e5eb654","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Hi there! How can I assist you today?","annotations":[]}]}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[],"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":37,"output_tokens":11,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":48},"user":null,"metadata":{}}} diff --git a/src/Testing/Responses/Fixtures/Responses/DeleteResponseFixture.php b/src/Testing/Responses/Fixtures/Responses/DeleteResponseFixture.php new file mode 100644 index 00000000..0ed29bba --- /dev/null +++ b/src/Testing/Responses/Fixtures/Responses/DeleteResponseFixture.php @@ -0,0 +1,12 @@ + 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', + 'object' => 'response', + 'deleted' => true, + ]; +} diff --git a/src/Testing/Responses/Fixtures/Responses/ListInputItemsFixture.php b/src/Testing/Responses/Fixtures/Responses/ListInputItemsFixture.php new file mode 100644 index 00000000..44576636 --- /dev/null +++ b/src/Testing/Responses/Fixtures/Responses/ListInputItemsFixture.php @@ -0,0 +1,28 @@ + 'list', + 'data' => [ + [ + 'type' => 'message', + 'id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'status' => 'completed', + 'role' => 'user', + 'content' => [ + [ + 'type' => 'text', + 'text' => 'What was a positive news story from today?', + 'annotations' => [], + ], + ], + ], + ], + 'first_id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'last_id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'has_more' => false, + ]; +} diff --git a/src/Testing/Responses/Fixtures/Responses/ResponseObjectFixture.php b/src/Testing/Responses/Fixtures/Responses/ResponseObjectFixture.php new file mode 100644 index 00000000..4d049b2f --- /dev/null +++ b/src/Testing/Responses/Fixtures/Responses/ResponseObjectFixture.php @@ -0,0 +1,103 @@ + 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', + 'object' => 'response', + 'created_at' => 1741484430, + 'status' => 'completed', + 'error' => null, + 'incomplete_details' => null, + 'instructions' => null, + 'max_output_tokens' => null, + 'model' => 'gpt-4o-2024-08-06', + 'output' => [ + [ + 'type' => 'web_search_call', + 'id' => 'ws_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'status' => 'completed', + ], + [ + 'type' => 'message', + 'id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'status' => 'completed', + 'role' => 'assistant', + 'content' => [ + [ + 'type' => 'output_text', + 'text' => 'As of today, March 9, 2025, one notable positive news story...', + 'annotations' => [ + [ + 'type' => 'url_citation', + 'start_index' => 442, + 'end_index' => 557, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + [ + 'type' => 'url_citation', + 'start_index' => 962, + 'end_index' => 1077, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + [ + 'type' => 'url_citation', + 'start_index' => 1336, + 'end_index' => 1451, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + ], + ], + ], + ], + ], + 'parallel_tool_calls' => true, + 'previous_response_id' => null, + 'reasoning' => [ + 'effort' => null, + 'generate_summary' => null, + ], + 'store' => true, + 'temperature' => 1.0, + 'text' => [ + 'format' => [ + 'type' => 'text', + ], + ], + 'tool_choice' => 'auto', + 'tools' => [ + [ + 'type' => 'web_search_preview', + 'domains' => [], + 'search_context_size' => 'medium', + 'user_location' => [ + 'type' => 'approximate', + 'city' => 'San Francisco', + 'country' => 'US', + 'region' => 'California', + 'timezone' => 'America/Los_Angeles', + ], + ], + ], + 'top_p' => 1.0, + 'truncation' => 'disabled', + 'usage' => [ + 'input_tokens' => 328, + 'input_tokens_details' => [ + 'cached_tokens' => 0, + ], + 'output_tokens' => 356, + 'output_tokens_details' => [ + 'reasoning_tokens' => 0, + ], + 'total_tokens' => 684, + ], + 'user' => null, + 'metadata' => [], + ]; +} diff --git a/src/Testing/Responses/Fixtures/Responses/RetrieveResponseFixture.php b/src/Testing/Responses/Fixtures/Responses/RetrieveResponseFixture.php new file mode 100644 index 00000000..9231f856 --- /dev/null +++ b/src/Testing/Responses/Fixtures/Responses/RetrieveResponseFixture.php @@ -0,0 +1,103 @@ + 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', + 'object' => 'response', + 'created_at' => 1741484430, + 'status' => 'completed', + 'error' => null, + 'incomplete_details' => null, + 'instructions' => null, + 'max_output_tokens' => null, + 'model' => 'gpt-4o-2024-08-06', + 'output' => [ + [ + 'type' => 'web_search_call', + 'id' => 'ws_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'status' => 'completed', + ], + [ + 'type' => 'message', + 'id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'status' => 'completed', + 'role' => 'assistant', + 'content' => [ + [ + 'type' => 'output_text', + 'text' => 'As of today, March 9, 2025, one notable positive news story...', + 'annotations' => [ + [ + 'type' => 'url_citation', + 'start_index' => 442, + 'end_index' => 557, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + [ + 'type' => 'url_citation', + 'start_index' => 962, + 'end_index' => 1077, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + [ + 'type' => 'url_citation', + 'start_index' => 1336, + 'end_index' => 1451, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + ], + ], + ], + ], + ], + 'parallel_tool_calls' => true, + 'previous_response_id' => null, + 'reasoning' => [ + 'effort' => null, + 'generate_summary' => null, + ], + 'store' => true, + 'temperature' => 1.0, + 'text' => [ + 'format' => [ + 'type' => 'text', + ], + ], + 'tool_choice' => 'auto', + 'tools' => [ + [ + 'type' => 'web_search_preview', + 'domains' => [], + 'search_context_size' => 'medium', + 'user_location' => [ + 'type' => 'approximate', + 'city' => 'San Francisco', + 'country' => 'US', + 'region' => 'California', + 'timezone' => 'America/Los_Angeles', + ], + ], + ], + 'top_p' => 1.0, + 'truncation' => 'disabled', + 'usage' => [ + 'input_tokens' => 328, + 'input_tokens_details' => [ + 'cached_tokens' => 0, + ], + 'output_tokens' => 356, + 'output_tokens_details' => [ + 'reasoning_tokens' => 0, + ], + 'total_tokens' => 684, + ], + 'user' => null, + 'metadata' => [], + ]; +} diff --git a/tests/Fixtures/Responses.php b/tests/Fixtures/Responses.php new file mode 100644 index 00000000..6d8a3c0a --- /dev/null +++ b/tests/Fixtures/Responses.php @@ -0,0 +1,301 @@ + + */ +function createResponseResource(): array +{ + return [ + 'id' => 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', + 'object' => 'response', + 'created_at' => 1741484430, + 'status' => 'completed', + 'error' => null, + 'incomplete_details' => null, + 'instructions' => null, + 'max_output_tokens' => null, + 'metadata' => [], + 'model' => 'gpt-4o-2024-08-06', + 'output' => [ + outputMessage(), + outputWebSearchToolCall(), + outputFileSearchToolCall(), + outputComputerToolCall(), + ], + 'parallel_tool_calls' => true, + 'previous_response_id' => null, + 'reasoning' => [ + 'effort' => null, + 'generate_summary' => null, + ], + 'store' => true, + 'temperature' => 1.0, + 'text' => [ + 'format' => [ + 'type' => 'text', + ], + ], + 'tool_choice' => 'auto', + 'tools' => [ + [ + 'type' => 'web_search_preview', + 'search_context_size' => 'medium', + 'user_location' => [ + 'type' => 'approximate', + 'city' => 'San Francisco', + 'country' => 'US', + 'region' => 'California', + 'timezone' => 'America/Los_Angeles', + ], + ], + ], + 'top_p' => 1.0, + 'truncation' => 'disabled', + 'usage' => [ + 'input_tokens' => 328, + 'input_tokens_details' => [ + 'cached_tokens' => 0, + ], + 'output_tokens' => 356, + 'output_tokens_details' => [ + 'reasoning_tokens' => 0, + ], + 'total_tokens' => 684, + ], + 'user' => null, + ]; +} + +/** + * @return array + */ +function retrieveResponseResource(): array +{ + return [ + 'id' => 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', + 'object' => 'response', + 'created_at' => 1741484430, + 'status' => 'completed', + 'error' => null, + 'incomplete_details' => null, + 'instructions' => null, + 'max_output_tokens' => null, + 'metadata' => [], + 'model' => 'gpt-4o-2024-08-06', + 'output' => [ + outputWebSearchToolCall(), + outputMessage(), + ], + 'parallel_tool_calls' => true, + 'previous_response_id' => null, + 'reasoning' => [ + 'effort' => null, + 'generate_summary' => null, + ], + 'store' => true, + 'temperature' => 1.0, + 'text' => [ + 'format' => [ + 'type' => 'text', + ], + ], + 'tool_choice' => 'auto', + 'tools' => [ + [ + 'type' => 'web_search_preview', + 'search_context_size' => 'medium', + 'user_location' => [ + 'type' => 'approximate', + 'city' => null, + 'country' => 'US', + 'region' => null, + 'timezone' => null, + ], + ], + ], + 'top_p' => 1.0, + 'truncation' => 'disabled', + 'usage' => [ + 'input_tokens' => 328, + 'input_tokens_details' => [ + 'cached_tokens' => 0, + ], + 'output_tokens' => 356, + 'output_tokens_details' => [ + 'reasoning_tokens' => 0, + ], + 'total_tokens' => 684, + ], + 'user' => null, + ]; +} + +/** + * @return array + */ +function listInputItemsResource(): array +{ + return [ + 'object' => 'list', + 'data' => [ + [ + 'type' => 'message', + 'id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'status' => 'completed', + 'role' => 'user', + 'content' => [ + [ + 'type' => 'text', + 'text' => 'What was a positive news story from today?', + 'annotations' => [], + ], + ], + ], + ], + 'first_id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'last_id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'has_more' => false, + ]; +} + +/** + * @return array + */ +function deleteResponseResource(): array +{ + return [ + 'id' => 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', + 'object' => 'response.deleted', + 'deleted' => true, + ]; +} + +/** + * @return array + */ +function createStreamedResponseResource(): array +{ + return [ + 'event' => 'response.created', + 'data' => createResponseResource(), + ]; +} + +/** + * @return array + */ +function outputFileSearchToolCall(): array +{ + return [ + 'id' => 'fs_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'queries' => [ + 'map', + 'kansas', + ], + 'status' => 'completed', + 'type' => 'file_search_call', + 'results' => [ + [ + 'attributes' => [ + 'foo' => 'bar', + ], + 'file_id' => 'file_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'filename' => 'kansas_map.geojson', + 'score' => 0.98882, + 'text' => 'Map of Kansas', + ], + ], + ]; +} + +/** + * @return array + */ +function outputComputerToolCall(): array +{ + return [ + 'type' => 'computer_call', + 'call_id' => 'call_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'id' => 'cu_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'action' => [ + 'button' => 'left', + 'type' => 'click', + 'x' => 117, + 'y' => 123, + ], + 'pending_safety_checks' => [ + [ + 'code' => 'malicious_instructions', + 'id' => 'cu_sc_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'message' => 'Safety check message', + ], + ], + 'status' => 'completed', + ]; +} + +/** + * @return array + */ +function outputWebSearchToolCall(): array +{ + return [ + 'id' => 'ws_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c', + 'status' => 'completed', + 'type' => 'web_search_call', + ]; +} + +/** + * @return array + */ +function outputMessage(): array +{ + return [ + 'content' => [ + [ + 'annotations' => [ + [ + 'end_index' => 557, + 'start_index' => 442, + 'title' => '...', + 'type' => 'url_citation', + 'url' => 'https://.../?utm_source=chatgpt.com', + ], + [ + 'end_index' => 1077, + 'start_index' => 962, + 'title' => '...', + 'type' => 'url_citation', + 'url' => 'https://.../?utm_source=chatgpt.com', + ], + [ + 'end_index' => 1451, + 'start_index' => 1336, + 'title' => '...', + 'type' => 'url_citation', + 'url' => 'https://.../?utm_source=chatgpt.com', + ], + ], + 'text' => 'As of today, March 9, 2025, one notable positive news story...', + 'type' => 'output_text', + ], + [ + 'refusal' => 'The assistant refused to answer.', + 'type' => 'refusal', + ], + ], + 'id' => 'msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c', + 'role' => 'assistant', + 'status' => 'completed', + 'type' => 'message', + ]; +} + +/** + * @return resource + */ +function responseCompletionStream() +{ + return fopen(__DIR__.'/Streams/ResponseCompletionCreate.txt', 'r'); +} diff --git a/tests/Fixtures/Streams/ResponseCompletionCreate.txt b/tests/Fixtures/Streams/ResponseCompletionCreate.txt new file mode 100644 index 00000000..e8f05b1e --- /dev/null +++ b/tests/Fixtures/Streams/ResponseCompletionCreate.txt @@ -0,0 +1,11 @@ +data: {"type":"response.created","response":{"id":"resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c","object":"response","created_at":1741484430,"status":"in_progress","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"generate_summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"web_search_preview","domains":[],"search_context_size":"medium","user_location":{"type":"approximate","city":null,"country":"US","region":null,"timezone":null}}],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} +data: {"type":"response.in_progress","response":{"id":"resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c","object":"response","created_at":1741484430,"status":"in_progress","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"generate_summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"web_search_preview","domains":[],"search_context_size":"medium","user_location":{"type":"approximate","city":null,"country":"US","region":null,"timezone":null}}],"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} +data: {"type":"response.output_item.added","output_index":0,"item":{"id":"ws_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c","type":"web_search_call","status":"in_progress"}} +data: {"type":"response.output_item.done","output_index":0,"item":{"id":"ws_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c","type":"web_search_call","status":"completed"}} +data: {"type":"response.output_item.added","output_index":1,"item":{"id":"msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c","type":"message","status":"in_progress","role":"assistant","content":[]}} +data: {"type":"response.content_part.added","item_id":"msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c","output_index":1,"content_index":0,"part":{"type":"output_text","text":"","annotations":[]}} +data: {"type":"response.output_text.delta","item_id":"msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c","output_index":1,"content_index":0,"delta":"As of today, March 9, 2025, one notable positive news story..."} +data: {"type":"response.output_text.done","item_id":"msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c","output_index":1,"content_index":0,"text":"As of today, March 9, 2025, one notable positive news story..."} +data: {"type":"response.content_part.done","item_id":"msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c","output_index":1,"content_index":0,"part":{"type":"output_text","text":"As of today, March 9, 2025, one notable positive news story...","annotations":[{"type":"url_citation","start_index":442,"end_index":557,"url":"https://.../?utm_source=chatgpt.com","title":"..."},{"type":"url_citation","start_index":962,"end_index":1077,"url":"https://.../?utm_source=chatgpt.com","title":"..."},{"type":"url_citation","start_index":1336,"end_index":1451,"url":"https://.../?utm_source=chatgpt.com","title":"..."}]}} +data: {"type":"response.output_item.done","output_index":1,"item":{"id":"msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"As of today, March 9, 2025, one notable positive news story...","annotations":[{"type":"url_citation","start_index":442,"end_index":557,"url":"https://.../?utm_source=chatgpt.com","title":"..."},{"type":"url_citation","start_index":962,"end_index":1077,"url":"https://.../?utm_source=chatgpt.com","title":"..."},{"type":"url_citation","start_index":1336,"end_index":1451,"url":"https://.../?utm_source=chatgpt.com","title":"..."}]}]}} +data: {"type":"response.completed","response":{"id":"resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c","object":"response","created_at":1741484430,"status":"completed","error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"model":"gpt-4o-2024-08-06","output":[{"type":"web_search_call","id":"ws_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c","status":"completed"},{"type":"message","id":"msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c","status":"completed","role":"assistant","content":[{"type":"output_text","text":"As of today, March 9, 2025, one notable positive news story...","annotations":[{"type":"url_citation","start_index":442,"end_index":557,"url":"https://.../?utm_source=chatgpt.com","title":"..."},{"type":"url_citation","start_index":962,"end_index":1077,"url":"https://.../?utm_source=chatgpt.com","title":"..."},{"type":"url_citation","start_index":1336,"end_index":1451,"url":"https://.../?utm_source=chatgpt.com","title":"..."}]}]}],"parallel_tool_calls":true,"previous_response_id":null,"reasoning":{"effort":null,"generate_summary":null},"store":true,"temperature":1.0,"text":{"format":{"type":"text"}},"tool_choice":"auto","tools":[{"type":"web_search_preview","domains":[],"search_context_size":"medium","user_location":{"type":"approximate","city":null,"country":"US","region":null,"timezone":null}}],"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":328,"input_tokens_details":{"cached_tokens":0},"output_tokens":356,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":684},"user":null,"metadata":{}}} diff --git a/tests/Resources/Responses.php b/tests/Resources/Responses.php new file mode 100644 index 00000000..cabc5048 --- /dev/null +++ b/tests/Resources/Responses.php @@ -0,0 +1,302 @@ + 'gpt-4o', + 'tools' => [['type' => 'web_search_preview']], + 'input' => 'what was a positive news story from today?', + ], \OpenAI\ValueObjects\Transporter\Response::from(createResponseResource(), metaHeaders())); + + $result = $client->responses()->create([ + 'model' => 'gpt-4o', + 'tools' => [['type' => 'web_search_preview']], + 'input' => 'what was a positive news story from today?', + ]); + + $output = $result->output; + expect($result) + ->toBeInstanceOf(CreateResponse::class) + ->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c') + ->object->toBe('response') + ->createdAt->toBe(1741484430) + ->status->toBe('completed') + ->error->toBeNull() + ->incompleteDetails->toBeNull() + ->instructions->toBeNull() + ->maxOutputTokens->toBeNull() + ->model->toBe('gpt-4o-2024-08-06') + ->output->toBeArray() + ->output->toHaveCount(4); + + expect($output[0]) + ->type->toBe('message') + ->id->toBe('msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c') + ->status->toBe('completed') + ->role->toBe('assistant') + ->content->toBeArray() + ->content->toHaveCount(2); + + expect($output[0]['content'][0]) + ->type->toBe('output_text') + ->text->toBe('As of today, March 9, 2025, one notable positive news story...'); + + expect($output[1]) + ->type->toBe('web_search_call') + ->id->toBe('ws_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c') + ->status->toBe('completed'); + + expect($result) + ->parallelToolCalls->toBeTrue() + ->previousResponseId->toBeNull() + ->temperature->toBe(1.0) + ->toolChoice->toBe('auto') + ->topP->toBe(1.0) + ->truncation->toBe('disabled'); + + expect($result->truncation) + ->toBe('disabled'); + + expect($result->meta()) + ->toBeInstanceOf(MetaInformation::class); +}); + +test('create streamed', function () { + $response = new Response( + body: new Stream(responseCompletionStream()), + headers: metaHeaders(), + ); + + $client = mockStreamClient('POST', 'responses', [ + 'model' => 'gpt-4o', + 'tools' => [['type' => 'web_search_preview']], + 'input' => 'what was a positive news story from today?', + 'stream' => true, + ], $response); + + $result = $client->responses()->createStreamed([ + 'model' => 'gpt-4o', + 'tools' => [['type' => 'web_search_preview']], + 'input' => 'what was a positive news story from today?', + ]); + + expect($result) + ->toBeInstanceOf(StreamResponse::class) + ->toBeInstanceOf(IteratorAggregate::class); + + expect($result->getIterator()) + ->toBeInstanceOf(Iterator::class); + + $current = $result->getIterator()->current(); + expect($current) + ->toBeInstanceOf(CreateStreamedResponse::class); + expect($current->event) + ->toBe('response.created'); + expect($current->response) + ->toBeInstanceOf(CreateResponse::class); + expect($current->response->id) + ->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); + expect($current->response->object) + ->toBe('response'); + expect($current->response->createdAt) + ->toBe(1741484430); + expect($current->response->status) + ->toBe('completed'); + expect($current->response->error) + ->toBeNull(); + expect($current->response->incompleteDetails) + ->toBeNull(); + expect($current->response->instructions) + ->toBeNull(); + expect($current->response->maxOutputTokens) + ->toBeNull(); + expect($current->response->model) + ->toBe('gpt-4o-2024-08-06'); + expect($current->response->output) + ->toBeArray(); + expect($current->response->output) + ->toHaveCount(2); + expect($current->response->output[0]->type) + ->toBe('web_search_call'); + expect($current->response->output[0]->id) + ->toBe('ws_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c'); + expect($current->response->output[0]->status) + ->toBe('completed'); + expect($current->response->output[1]->type) + ->toBe('message'); + expect($current->response->output[1]->id) + ->toBe('msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c'); + expect($current->response->output[1]->status) + ->toBe('completed'); + expect($current->response->output[1]->role) + ->toBe('assistant'); + expect($current->response->output[1]->content) + ->toBeArray(); + expect($current->response->output[1]->content) + ->toHaveCount(1); + expect($current->response->output[1]->content[0]->type) + ->toBe('output_text'); + expect($current->response->output[1]->content[0]->text) + ->toBe('As of today, March 9, 2025, one notable positive news story...'); + expect($current->response->output[1]->content[0]->annotations) + ->toBeArray(); + expect($current->response->output[1]->content[0]->annotations) + ->toHaveCount(3); + expect($current->response->output[1]->content[0]->annotations[0]->type) + ->toBe('url_citation'); + expect($current->response->output[1]->content[0]->annotations[0]->startIndex) + ->toBe(442); + expect($current->response->output[1]->content[0]->annotations[0]->endIndex) + ->toBe(557); + expect($current->response->output[1]->content[0]->annotations[0]->url) + ->toBe('https://.../?utm_source=chatgpt.com'); + expect($current->response->output[1]->content[0]->annotations[0]->title) + ->toBe('...'); + expect($current->response->output[1]->content[0]->annotations[1]->type) + ->toBe('url_citation'); + expect($current->response->output[1]->content[0]->annotations[1]->startIndex) + ->toBe(962); + expect($current->response->output[1]->content[0]->annotations[1]->endIndex) + ->toBe(1077); + expect($current->response->output[1]->content[0]->annotations[1]->url) + ->toBe('https://.../?utm_source=chatgpt.com'); + expect($current->response->output[1]->content[0]->annotations[1]->title) + ->toBe('...'); + expect($current->response->output[1]->content[0]->annotations[2]->type) + ->toBe('url_citation'); + expect($current->response->output[1]->content[0]->annotations[2]->startIndex) + ->toBe(1336); + expect($current->response->output[1]->content[0]->annotations[2]->endIndex) + ->toBe(1451); + expect($current->response->output[1]->content[0]->annotations[2]->url) + ->toBe('https://.../?utm_source=chatgpt.com'); + expect($current->response->output[1]->content[0]->annotations[2]->title) + ->toBe('...'); + expect($current->response->parallelToolCalls) + ->toBeTrue(); + expect($current->response->previousResponseId) + ->toBeNull(); + expect($current->response->temperature) + ->toBe(1.0); + expect($current->response->toolChoice) + ->toBe('auto'); + expect($current->response->topP) + ->toBe(1.0); + expect($current->response->truncation) + ->toBe('disabled'); + expect($current->response->reasoning) + ->toBeArray(); + expect($current->response->reasoning['effort']) + ->toBeNull(); + expect($current->response->reasoning['generate_summary']) + ->toBeNull(); + expect($current->response->text) + ->toBeArray(); + expect($current->response->text['format']['type']) + ->toBe('text'); + expect($current->response->tools) + ->toBeArray(); + expect($current->response->tools) + ->toHaveCount(1); + expect($current->response->tools[0]->type) + ->toBe('web_search_preview'); + expect($current->response->tools[0]->domains) + ->toBeArray()->toBeEmpty(); + expect($current->response->tools[0]->searchContextSize) + ->toBe('medium'); + expect($current->response->tools[0]->userLocation) + ->toBeArray(); + expect($current->response->tools[0]->userLocation['type']) + ->toBe('approximate'); + expect($current->response->tools[0]->userLocation['city']) + ->toBeNull(); + expect($current->response->tools[0]->userLocation['country']) + ->toBe('US'); + expect($current->response->tools[0]->userLocation['region']) + ->toBeNull(); + expect($current->response->tools[0]->userLocation['timezone']) + ->toBeNull(); + expect($current->response->usage) + ->toBeArray(); + expect($current->response->usage['input_tokens']) + ->toBe(328); + expect($current->response->usage['input_tokens_details']['cached_tokens']) + ->toBe(0); + expect($current->response->usage['output_tokens']) + ->toBe(356); + expect($current->response->usage['output_tokens_details']['reasoning_tokens']) + ->toBe(0); + expect($current->response->usage['total_tokens']) + ->toBe(684); + expect($current->response->user) + ->toBeNull(); + expect($current->response->metadata) + ->toBeArray(); + expect($current->response->metadata) + ->toBeEmpty(); + expect($current->response->truncation) + ->toBe('disabled'); + + expect($result->meta()) + ->toBeInstanceOf(MetaInformation::class); +}); + +test('delete', function () { + $client = mockClient('DELETE', 'responses/resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', [ + ], \OpenAI\ValueObjects\Transporter\Response::from(deleteResponseResource(), metaHeaders())); + + $result = $client->responses()->delete('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); + + expect($result) + ->toBeInstanceOf(DeleteResponse::class) + ->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c') + ->object->toBe('response.deleted') + ->deleted->toBeTrue(); + + expect($result->meta()) + ->toBeInstanceOf(MetaInformation::class); +}); + +test('list', function () { + $client = mockClient('GET', 'responses/resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c/input_items', [ + ], \OpenAI\ValueObjects\Transporter\Response::from(listInputItemsResource(), metaHeaders())); + + $result = $client->responses()->list('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); + + expect($result) + ->toBeInstanceOf(ListInputItems::class) + ->object->toBe('list') + ->data->toBeArray() + ->firstId->toBe('msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c') + ->lastId->toBe('msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c') + ->hasMore->toBeFalse(); + + expect($result->meta()) + ->toBeInstanceOf(MetaInformation::class); +}); + +test('retrieve', function () { + $client = mockClient('GET', 'responses/resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', [ + ], \OpenAI\ValueObjects\Transporter\Response::from(retrieveResponseResource(), metaHeaders())); + + $result = $client->responses()->retrieve('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); + + expect($result) + ->toBeInstanceOf(RetrieveResponse::class) + ->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c') + ->object->toBe('response') + ->createdAt->toBe(1741484430) + ->status->toBe('completed'); + + expect($result->meta()) + ->toBeInstanceOf(MetaInformation::class); +}); diff --git a/tests/Responses/Responses/CreateResponse.php b/tests/Responses/Responses/CreateResponse.php new file mode 100644 index 00000000..e09e875a --- /dev/null +++ b/tests/Responses/Responses/CreateResponse.php @@ -0,0 +1,74 @@ +toBeInstanceOf(CreateResponse::class) + ->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c') + ->object->toBe('response') + ->createdAt->toBe(1741484430) + ->status->toBe('completed') + ->error->toBeNull() + ->incompleteDetails->toBeNull() + ->instructions->toBeNull() + ->maxOutputTokens->toBeNull() + ->model->toBe('gpt-4o-2024-08-06') + ->output->toBeArray() + ->parallelToolCalls->toBeTrue() + ->previousResponseId->toBeNull() + ->reasoning->toBeInstanceOf(CreateResponseReasoning::class) + ->store->toBeTrue() + ->temperature->toBe(1.0) + ->text->toBeInstanceOf(CreateResponseFormat::class) + ->toolChoice->toBe('auto') + ->tools->toBeArray() + ->topP->toBe(1.0) + ->truncation->toBe('disabled') + ->usage->toBeInstanceOf(CreateResponseUsage::class) + ->user->toBeNull() + ->metadata->toBe([]); + + expect($response->meta()) + ->toBeInstanceOf(MetaInformation::class); +}); + +test('as array accessible', function () { + $response = CreateResponse::from(createResponseResource(), meta()); + + expect($response['id'])->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); +}); + +test('to array', function () { + $response = CreateResponse::from(createResponseResource(), meta()); + + expect($response->toArray()) + ->toBeArray() + ->toBe(createResponseResource()); +}); + +test('fake', function () { + $response = CreateResponse::fake(); + + expect($response) + ->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); +}); + +test('fake with override', function () { + $response = CreateResponse::fake([ + 'id' => 'resp_1234', + 'object' => 'custom_response', + 'status' => 'failed', + ]); + + expect($response) + ->id->toBe('resp_1234') + ->object->toBe('custom_response') + ->status->toBe('failed'); +}); diff --git a/tests/Responses/Responses/CreateStreamedResponse.php b/tests/Responses/Responses/CreateStreamedResponse.php new file mode 100644 index 00000000..627e0999 --- /dev/null +++ b/tests/Responses/Responses/CreateStreamedResponse.php @@ -0,0 +1,110 @@ + 'response.created', + '__meta' => meta(), + 'response' => [ + 'id' => 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', + 'object' => 'response', + 'created_at' => 1741484430, + 'status' => 'completed', + 'output' => [ + [ + 'type' => 'message', + 'id' => 'msg_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', + 'status' => 'completed', + 'role' => 'assistant', + 'content' => [ + [ + 'type' => 'output_text', + 'text' => 'As of today, March 9, 2025, one notable positive news story...', + 'annotations' => [ + [ + 'type' => 'url_citation', + 'start_index' => 442, + 'end_index' => 557, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + [ + 'type' => 'url_citation', + 'start_index' => 962, + 'end_index' => 1077, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + [ + 'type' => 'url_citation', + 'start_index' => 1336, + 'end_index' => 1451, + 'url' => 'https://.../?utm_source=chatgpt.com', + 'title' => '...', + ], + ], + ], + ], + ], + ], + ], + ]); + + expect($response) + ->toBeInstanceOf(CreateStreamedResponse::class) + ->event->toBe('response.created') + ->response->toBeInstanceOf(CreateResponse::class) + ->response->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); +}); + +test('as array accessible', function () { + $response = CreateStreamedResponse::from([ + '__event' => 'response.created', + '__meta' => meta(), + 'response' => [ + 'id' => 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', + ], + ]); + + expect($response['event'])->toBe('response.created'); +}); + +test('to array', function () { + $response = CreateStreamedResponse::from([ + '__event' => 'response.created', + '__meta' => meta(), + 'response' => [ + 'id' => 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', + ], + ]); + + expect($response->toArray()) + ->toBeArray() + ->toBe([ + 'event' => 'response.created', + 'response' => [ + 'id' => 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', + ], + ]); +}); + +test('fake', function () { + $response = CreateStreamedResponse::fake(); + + expect($response) + ->event->toBe('response.created') + ->response->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); +}); + +test('fake with override', function () { + $response = CreateStreamedResponse::fake([ + 'event' => 'response.completed', + 'response' => ['id' => 'resp_1234'], + ]); + + expect($response) + ->event->toBe('response.completed') + ->response->id->toBe('resp_1234'); +}); diff --git a/tests/Responses/Responses/DeleteResponse.php b/tests/Responses/Responses/DeleteResponse.php new file mode 100644 index 00000000..a6796d2d --- /dev/null +++ b/tests/Responses/Responses/DeleteResponse.php @@ -0,0 +1,47 @@ +id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c') + ->object->toBe('response.deleted') + ->deleted->toBe(true) + ->meta()->toBeInstanceOf(MetaInformation::class); +}); + +test('as array accessible', function () { + $result = DeleteResponse::from(deleteResponseResource(), meta()); + + expect($result['id']) + ->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); +}); + +test('to array', function () { + $result = DeleteResponse::from(deleteResponseResource(), meta()); + + expect($result->toArray()) + ->toBe(deleteResponseResource()); +}); + +test('fake', function () { + $response = DeleteResponse::fake(); + + expect($response) + ->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c') + ->deleted->toBe(true); +}); + +test('fake with override', function () { + $response = DeleteResponse::fake([ + 'id' => 'resp_1234', + 'deleted' => false, + ]); + + expect($response) + ->id->toBe('resp_1234') + ->deleted->toBe(false); +}); diff --git a/tests/Responses/Responses/ListInputItems.php b/tests/Responses/Responses/ListInputItems.php new file mode 100644 index 00000000..95daaf47 --- /dev/null +++ b/tests/Responses/Responses/ListInputItems.php @@ -0,0 +1,53 @@ +toBeInstanceOf(ListInputItems::class) + ->object->toBe('list') + ->data->toBeArray() + ->firstId->toBe('msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c') + ->lastId->toBe('msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c') + ->hasMore->toBeFalse() + ->meta()->toBeInstanceOf(MetaInformation::class); +}); + +test('as array accessible', function () { + $result = ListInputItems::from(listInputItemsResource(), meta()); + + expect($result['object'])->toBe('list'); +}); + +test('to array', function () { + $result = ListInputItems::from(listInputItemsResource(), meta()); + + expect($result->toArray()) + ->toBeArray() + ->toBe(listInputItemsResource()); +}); + +test('fake', function () { + $response = ListInputItems::fake(); + + expect($response) + ->object->toBe('list') + ->firstId->toBe('msg_67ccf190ca3881909d433c50b1f6357e087bb177ab789d5c') + ->hasMore->toBeFalse(); +}); + +test('fake with override', function () { + $response = ListInputItems::fake([ + 'object' => 'custom_list', + 'first_id' => 'msg_1234', + 'has_more' => true, + ]); + + expect($response) + ->object->toBe('custom_list') + ->firstId->toBe('msg_1234') + ->hasMore->toBeTrue(); +}); diff --git a/tests/Responses/Responses/Output/OutputComputerToolCall.php b/tests/Responses/Responses/Output/OutputComputerToolCall.php new file mode 100644 index 00000000..d4b6ba94 --- /dev/null +++ b/tests/Responses/Responses/Output/OutputComputerToolCall.php @@ -0,0 +1,30 @@ +toBeInstanceOf(OutputComputerToolCall::class) + ->action->toBeInstanceOf(OutputComputerActionClick::class) + ->callId->toBe('call_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c') + ->id->toBe('cu_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c') + ->status->toBe('completed') + ->pendingSafetyChecks->toBeArray(); +}); + +test('as array accessible', function () { + $response = OutputComputerToolCall::from(outputComputerToolCall()); + + expect($response['id'])->toBe('cu_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c'); +}); + +test('to array', function () { + $response = OutputComputerToolCall::from(outputComputerToolCall()); + + expect($response->toArray()) + ->toBeArray() + ->toBe(outputComputerToolCall()); +}); diff --git a/tests/Responses/Responses/Output/OutputFileSearchToolCall.php b/tests/Responses/Responses/Output/OutputFileSearchToolCall.php new file mode 100644 index 00000000..b92548f7 --- /dev/null +++ b/tests/Responses/Responses/Output/OutputFileSearchToolCall.php @@ -0,0 +1,42 @@ +toBeInstanceOf(OutputFileSearchToolCall::class) + ->id->toBe('fs_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c') + ->queries->toBe(['map', 'kansas']) + ->status->toBe('completed') + ->type->toBe('file_search_call') + ->results->toBeArray(); +}); + +test('from results', function () { + $response = OutputFileSearchToolCallResult::from(outputFileSearchToolCall()['results'][0]); + + expect($response) + ->toBeInstanceOf(OutputFileSearchToolCallResult::class) + ->attributes->toBe(['foo' => 'bar']) + ->fileId->toBe('file_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c') + ->filename->toBe('kansas_map.geojson') + ->score->toBe(0.98882) + ->text->toBe('Map of Kansas'); +}); + +test('as array accessible', function () { + $response = OutputFileSearchToolCall::from(outputFileSearchToolCall()); + + expect($response['id'])->toBe('fs_67ccf18f64008190a39b619f4c8455ef087bb177ab789d5c'); +}); + +test('to array', function () { + $response = OutputFileSearchToolCall::from(outputFileSearchToolCall()); + + expect($response->toArray()) + ->toBeArray() + ->toBe(outputFileSearchToolCall()); +}); diff --git a/tests/Responses/Responses/RetrieveResponse.php b/tests/Responses/Responses/RetrieveResponse.php new file mode 100644 index 00000000..bc72cf1c --- /dev/null +++ b/tests/Responses/Responses/RetrieveResponse.php @@ -0,0 +1,77 @@ +toBeInstanceOf(RetrieveResponse::class) + ->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c') + ->object->toBe('response') + ->createdAt->toBe(1741484430) + ->status->toBe('completed') + ->error->toBeNull() + ->incompleteDetails->toBeNull() + ->instructions->toBeNull() + ->maxOutputTokens->toBeNull() + ->model->toBe('gpt-4o-2024-08-06') + ->output->toBeArray() + ->output->toHaveCount(2) + ->parallelToolCalls->toBeTrue() + ->previousResponseId->toBeNull() + ->reasoning->toBeInstanceOf(CreateResponseReasoning::class) + ->store->toBeTrue() + ->temperature->toBe(1.0) + ->text->toBeInstanceOf(CreateResponseFormat::class) + ->toolChoice->toBe('auto') + ->tools->toBeArray() + ->tools->toHaveCount(1) + ->topP->toBe(1.0) + ->truncation->toBe('disabled') + ->usage->toBeInstanceOf(CreateResponseUsage::class) + ->user->toBeNull() + ->metadata->toBe([]); + + expect($result->meta()) + ->toBeInstanceOf(MetaInformation::class); +}); + +test('as array accessible', function () { + $result = RetrieveResponse::from(retrieveResponseResource(), meta()); + + expect($result['id'])->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); +}); + +test('to array', function () { + $result = RetrieveResponse::from(retrieveResponseResource(), meta()); + + expect($result->toArray()) + ->toBe(retrieveResponseResource()); +}); + +test('fake', function () { + $response = RetrieveResponse::fake(); + + expect($response) + ->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c') + ->object->toBe('response') + ->status->toBe('completed'); +}); + +test('fake with override', function () { + $response = RetrieveResponse::fake([ + 'id' => 'resp_1234', + 'object' => 'custom_response', + 'status' => 'failed', + ]); + + expect($response) + ->id->toBe('resp_1234') + ->object->toBe('custom_response') + ->status->toBe('failed'); +}); diff --git a/tests/Testing/ClientFakeResponses.php b/tests/Testing/ClientFakeResponses.php new file mode 100644 index 00000000..8db5636f --- /dev/null +++ b/tests/Testing/ClientFakeResponses.php @@ -0,0 +1,158 @@ + 'gpt-4o', + 'tools' => [['type' => 'web_search_preview']], + 'input' => 'what was a positive news story from today?', + ]), + ]); + + $response = $fake->responses()->create([ + 'model' => 'gpt-4o', + 'tools' => [['type' => 'web_search_preview']], + 'input' => 'what was a positive news story from today?', + ]); + + expect($response['model'])->toBe('gpt-4o'); + expect($response['tools'][0]['type'])->toBe('web_search_preview'); +}); + +it('returns a fake response for retrieve', function () { + $fake = new ClientFake([ + RetrieveResponse::fake([ + 'id' => 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c', + ]), + ]); + + $response = $fake->responses()->retrieve('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); + + expect($response) + ->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c') + ->object->toBe('response'); +}); + +it('returns a fake response for delete', function () { + $fake = new ClientFake([ + DeleteResponse::fake(), + ]); + + $response = $fake->responses()->delete('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); + + expect($response) + ->id->toBe('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c') + ->deleted->toBeTrue(); +}); + +it('returns a fake response for list', function () { + $fake = new ClientFake([ + ListInputItems::fake(), + ]); + + $response = $fake->responses()->list('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); + + expect($response->data)->toBeArray(); +}); + +it('asserts a create request was sent', function () { + $fake = new ClientFake([ + CreateResponse::fake(), + ]); + + $fake->responses()->create([ + 'model' => 'gpt-4o', + 'tools' => [['type' => 'web_search_preview']], + 'input' => 'what was a positive news story from today?', + ]); + + $fake->assertSent(Responses::class, function ($method, $parameters) { + return $method === 'create' && + $parameters['model'] === 'gpt-4o' && + $parameters['tools'][0]['type'] === 'web_search_preview' && + $parameters['input'] === 'what was a positive news story from today?'; + }); +}); + +it('asserts a retrieve request was sent', function () { + $fake = new ClientFake([ + RetrieveResponse::fake(), + ]); + + $fake->responses()->retrieve('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); + + $fake->assertSent(Responses::class, function ($method, $responseId) { + return $method === 'retrieve' && + $responseId === 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'; + }); +}); + +it('asserts a delete request was sent', function () { + $fake = new ClientFake([ + DeleteResponse::fake(), + ]); + + $fake->responses()->delete('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); + + $fake->assertSent(Responses::class, function ($method, $responseId) { + return $method === 'delete' && + $responseId === 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'; + }); +}); + +it('asserts a list request was sent', function () { + $fake = new ClientFake([ + ListInputItems::fake(), + ]); + + $fake->responses()->list('resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'); + + $fake->assertSent(Responses::class, function ($method, $responseId) { + return $method === 'list' && + $responseId === 'resp_67ccf18ef5fc8190b16dbee19bc54e5f087bb177ab789d5c'; + }); +}); + +it('throws an exception if there are no more fake responses', function () { + $fake = new ClientFake([ + CreateResponse::fake(), + ]); + + $fake->responses()->create([ + 'model' => 'gpt-4o', + 'tools' => [ + [ + 'type' => 'web_search_preview', + ], + ], + 'input' => 'what was a positive news story from today?', + ]); + + $fake->responses()->create([ + 'model' => 'gpt-4o', + 'tools' => [ + [ + 'type' => 'web_search_preview', + ], + ], + 'input' => 'what was a positive news story from today?', + ]); +})->expectExceptionMessage('No fake responses left'); + +it('throws an exception if a request was not sent', function () { + $fake = new ClientFake([ + CreateResponse::fake(), + ]); + + $fake->assertSent(Responses::class, function ($method, $parameters) { + return $method === 'create'; + }); +})->expectException(ExpectationFailedException::class); diff --git a/tests/Testing/Resources/ResponsesTestResource.php b/tests/Testing/Resources/ResponsesTestResource.php new file mode 100644 index 00000000..705f04e7 --- /dev/null +++ b/tests/Testing/Resources/ResponsesTestResource.php @@ -0,0 +1,74 @@ +responses()->create([ + 'model' => 'gpt-4o-mini', + 'tools' => [ + [ + 'type' => 'web_search_preview', + ], + ], + 'input' => 'what was a positive news story from today?', + ]); + + $fake->assertSent(Responses::class, function ($method, $parameters) { + return $method === 'create' && + $parameters['model'] === 'gpt-4o-mini' && + $parameters['tools'] === [ + [ + 'type' => 'web_search_preview', + ], + ] && + $parameters['input'] === 'what was a positive news story from today?'; + }); +}); + +it('records a response retrieve request', function () { + $fake = new ClientFake([ + RetrieveResponse::fake(), + ]); + + $fake->responses()->retrieve('asst_SMzoVX8XmCZEg1EbMHoAm8tc'); + + $fake->assertSent(Responses::class, function ($method, $responseId) { + return $method === 'retrieve' && + $responseId === 'asst_SMzoVX8XmCZEg1EbMHoAm8tc'; + }); +}); + +it('records a response delete request', function () { + $fake = new ClientFake([ + DeleteResponse::fake(), + ]); + + $fake->responses()->delete('asst_SMzoVX8XmCZEg1EbMHoAm8tc'); + + $fake->assertSent(Responses::class, function ($method, $responseId) { + return $method === 'delete' && + $responseId === 'asst_SMzoVX8XmCZEg1EbMHoAm8tc'; + }); +}); + +it('records a response list request', function () { + $fake = new ClientFake([ + ListInputItems::fake(), + ]); + + $fake->responses()->list('asst_SMzoVX8XmCZEg1EbMHoAm8tc'); + + $fake->assertSent(Responses::class, function ($method, $responseId) { + return $method === 'list' && + $responseId === 'asst_SMzoVX8XmCZEg1EbMHoAm8tc'; + }); +});