Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/Responses/Responses/Output/OutputWebSearchToolCall.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@

/**
* @phpstan-import-type WebSearchActionType from OutputWebSearchAction
* @phpstan-import-type OutputWebSearchToolCallResultType from OutputWebSearchToolCallResult
*
* @phpstan-type OutputWebSearchToolCallType array{id: string, status: string, type: 'web_search_call', action?: WebSearchActionType}
* @phpstan-type OutputWebSearchToolCallType array{id: string, status: string, type: 'web_search_call', action?: WebSearchActionType, results?: array<int, OutputWebSearchToolCallResultType>}
*
* @implements ResponseContract<OutputWebSearchToolCallType>
*/
Expand All @@ -27,12 +28,14 @@ final class OutputWebSearchToolCall implements ResponseContract

/**
* @param 'web_search_call' $type
* @param ?array<int, OutputWebSearchToolCallResult> $results
*/
private function __construct(
public readonly string $id,
public readonly string $status,
public readonly string $type,
public readonly ?OutputWebSearchAction $action,
public readonly ?array $results,
) {}

/**
Expand All @@ -46,7 +49,13 @@ public static function from(array $attributes): self
type: $attributes['type'],
action: isset($attributes['action'])
? OutputWebSearchAction::from($attributes['action'])
: null
: null,
results: isset($attributes['results'])
? array_map(
static fn (array $result): OutputWebSearchToolCallResult => OutputWebSearchToolCallResult::from($result),
$attributes['results'],
)
: null,
);
}

Expand All @@ -65,6 +74,13 @@ public function toArray(): array
$data['action'] = $this->action->toArray();
}

if ($this->results !== null) {
$data['results'] = array_map(
static fn (OutputWebSearchToolCallResult $result): array => $result->toArray(),
$this->results,
);
}

return $data;
}
}
72 changes: 72 additions & 0 deletions src/Responses/Responses/Output/OutputWebSearchToolCallResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace OpenAI\Responses\Responses\Output;

use OpenAI\Contracts\ResponseContract;
use OpenAI\Responses\Concerns\ArrayAccessible;
use OpenAI\Testing\Responses\Concerns\Fakeable;

/**
* @phpstan-type OutputWebSearchToolCallResultType array{type: 'image_result', image_url: string, source_website_url: string, thumbnail_url?: string, caption?: string}
*
* @implements ResponseContract<OutputWebSearchToolCallResultType>
*/
final class OutputWebSearchToolCallResult implements ResponseContract
{
/**
* @use ArrayAccessible<OutputWebSearchToolCallResultType>
*/
use ArrayAccessible;

use Fakeable;

/**
* @param 'image_result' $type
*/
private function __construct(
public readonly string $type,
public readonly string $imageUrl,
public readonly string $sourceWebsiteUrl,
public readonly ?string $thumbnailUrl,
public readonly ?string $caption,
) {}

/**
* @param OutputWebSearchToolCallResultType $attributes
*/
public static function from(array $attributes): self
{
return new self(
type: $attributes['type'],
imageUrl: $attributes['image_url'],
sourceWebsiteUrl: $attributes['source_website_url'],
thumbnailUrl: $attributes['thumbnail_url'] ?? null,
caption: $attributes['caption'] ?? null,
);
}

/**
* {@inheritDoc}
*/
public function toArray(): array
{
$data = [
'type' => $this->type,
'image_url' => $this->imageUrl,
];

if ($this->thumbnailUrl !== null) {
$data['thumbnail_url'] = $this->thumbnailUrl;
}

$data['source_website_url'] = $this->sourceWebsiteUrl;

if ($this->caption !== null) {
$data['caption'] = $this->caption;
}

return $data;
}
}
58 changes: 58 additions & 0 deletions src/Responses/Responses/Tool/WebSearchImageSettings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

namespace OpenAI\Responses\Responses\Tool;

use OpenAI\Contracts\ResponseContract;
use OpenAI\Responses\Concerns\ArrayAccessible;
use OpenAI\Testing\Responses\Concerns\Fakeable;

/**
* @phpstan-type WebSearchImageSettingsType array{max_results?: int, caption?: bool}
*
* @implements ResponseContract<WebSearchImageSettingsType>
*/
final class WebSearchImageSettings implements ResponseContract
{
/**
* @use ArrayAccessible<WebSearchImageSettingsType>
*/
use ArrayAccessible;

use Fakeable;

private function __construct(
public readonly ?int $maxResults,
public readonly ?bool $caption,
) {}

/**
* @param WebSearchImageSettingsType $attributes
*/
public static function from(array $attributes): self
{
return new self(
maxResults: $attributes['max_results'] ?? null,
caption: $attributes['caption'] ?? null,
);
}

/**
* {@inheritDoc}
*/
public function toArray(): array
{
$data = [];

if ($this->maxResults !== null) {
$data['max_results'] = $this->maxResults;
}

if ($this->caption !== null) {
$data['caption'] = $this->caption;
}

return $data;
}
}
22 changes: 20 additions & 2 deletions src/Responses/Responses/Tool/WebSearchTool.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
use OpenAI\Testing\Responses\Concerns\Fakeable;

/**
* @phpstan-import-type WebSearchImageSettingsType from WebSearchImageSettings
* @phpstan-import-type UserLocationType from WebSearchUserLocation
*
* @phpstan-type WebSearchToolType array{type: 'web_search'|'web_search_preview'|'web_search_preview_2025_03_11', search_context_size: 'low'|'medium'|'high', user_location: ?UserLocationType}
* @phpstan-type WebSearchToolType array{type: 'web_search'|'web_search_preview'|'web_search_preview_2025_03_11', search_context_size: 'low'|'medium'|'high', user_location: ?UserLocationType, search_content_types?: array<int, 'image'|'text'>, image_settings?: WebSearchImageSettingsType}
*
* @implements ResponseContract<WebSearchToolType>
*/
Expand All @@ -27,11 +28,14 @@ final class WebSearchTool implements ResponseContract
/**
* @param 'web_search'|'web_search_preview'|'web_search_preview_2025_03_11' $type
* @param 'low'|'medium'|'high' $searchContextSize
* @param ?array<int, 'image'|'text'> $searchContentTypes
*/
private function __construct(
public readonly string $type,
public readonly string $searchContextSize,
public readonly ?WebSearchUserLocation $userLocation,
public readonly ?array $searchContentTypes,
public readonly ?WebSearchImageSettings $imageSettings,
) {}

/**
Expand All @@ -45,6 +49,10 @@ public static function from(array $attributes): self
userLocation: isset($attributes['user_location'])
? WebSearchUserLocation::from($attributes['user_location'])
: null,
searchContentTypes: $attributes['search_content_types'] ?? null,
imageSettings: isset($attributes['image_settings'])
? WebSearchImageSettings::from($attributes['image_settings'])
: null,
);
}

Expand All @@ -53,10 +61,20 @@ public static function from(array $attributes): self
*/
public function toArray(): array
{
return [
$data = [
'type' => $this->type,
'search_context_size' => $this->searchContextSize,
'user_location' => $this->userLocation?->toArray(),
];

if ($this->searchContentTypes !== null) {
$data['search_content_types'] = $this->searchContentTypes;
}

if ($this->imageSettings !== null) {
$data['image_settings'] = $this->imageSettings->toArray();
}

return $data;
}
}
14 changes: 14 additions & 0 deletions tests/Fixtures/Responses.php
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,15 @@ function outputWebSearchToolCall(): array
],
'query' => 'what was a positive news story from today?',
],
'results' => [
[
'type' => 'image_result',
'image_url' => 'https://example.com/images/positive-story.jpg',
'thumbnail_url' => 'https://example.com/images/positive-story-thumbnail.jpg',
'source_website_url' => 'https://example.com/news/positive-story',
'caption' => 'A positive news story',
],
],
];
}

Expand Down Expand Up @@ -897,6 +906,11 @@ function toolWebSearchPreview(): array
'region' => 'California',
'timezone' => 'America/Los_Angeles',
],
'search_content_types' => ['image', 'text'],
'image_settings' => [
'max_results' => 3,
'caption' => true,
],
];
}

Expand Down
45 changes: 44 additions & 1 deletion tests/Responses/Responses/Output/OutputWebSearchToolCall.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use OpenAI\Responses\Responses\Output\OutputWebSearchToolCall;
use OpenAI\Responses\Responses\Output\OutputWebSearchToolCallResult;
use OpenAI\Responses\Responses\Output\WebSearch\OutputWebSearchAction;

test('from with full action', function () {
Expand All @@ -14,12 +15,22 @@
->action->toBeInstanceOf(OutputWebSearchAction::class)
->action->query->toBe('what was a positive news story from today?')
->action->sources->toBeArray()
->action->sources->toHaveCount(2);
->action->sources->toHaveCount(2)
->results->toBeArray()
->results->toHaveCount(1);

// Ensure first source is parsed correctly
expect($response->action->sources[0])
->type->toBe('url')
->url->toBe('https://example.com/news/positive-story');

expect($response->results[0])
->toBeInstanceOf(OutputWebSearchToolCallResult::class)
->type->toBe('image_result')
->imageUrl->toBe('https://example.com/images/positive-story.jpg')
->thumbnailUrl->toBe('https://example.com/images/positive-story-thumbnail.jpg')
->sourceWebsiteUrl->toBe('https://example.com/news/positive-story')
->caption->toBe('A positive news story');
});

test('as array accessible', function () {
Expand Down Expand Up @@ -55,6 +66,38 @@
->not->toHaveKey('action');
});

test('from without results', function () {
$payload = outputWebSearchToolCall();
unset($payload['results']);

$response = OutputWebSearchToolCall::from($payload);

expect($response)
->toBeInstanceOf(OutputWebSearchToolCall::class)
->results->toBeNull();

expect($response->toArray())
->toBeArray()
->toBe($payload)
->not->toHaveKey('results');
});

test('from result without optional fields', function () {
$payload = outputWebSearchToolCall();
unset($payload['results'][0]['thumbnail_url'], $payload['results'][0]['caption']);

$response = OutputWebSearchToolCall::from($payload);

expect($response->results[0])
->toBeInstanceOf(OutputWebSearchToolCallResult::class)
->thumbnailUrl->toBeNull()
->caption->toBeNull();

expect($response->toArray())
->toBeArray()
->toBe($payload);
});

test('from with action but without query', function () {
$payload = outputWebSearchToolCall();
unset($payload['action']['query']);
Expand Down
38 changes: 38 additions & 0 deletions tests/Responses/Responses/Tool/WebSearchImageSettings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

use OpenAI\Responses\Responses\Tool\WebSearchImageSettings;

test('from', function () {
$response = WebSearchImageSettings::from([
'max_results' => 3,
'caption' => true,
]);

expect($response)
->toBeInstanceOf(WebSearchImageSettings::class)
->maxResults->toBe(3)
->caption->toBeTrue();
});

test('from without optional keys', function () {
$response = WebSearchImageSettings::from([]);

expect($response)
->toBeInstanceOf(WebSearchImageSettings::class)
->maxResults->toBeNull()
->caption->toBeNull()
->toArray()->toBe([]);
});

test('preserves a disabled caption setting', function () {
$response = WebSearchImageSettings::from([
'caption' => false,
]);

expect($response)
->maxResults->toBeNull()
->caption->toBeFalse()
->toArray()->toBe([
'caption' => false,
]);
});
Loading
Loading