Skip to content

feat(OpenAI): Support annotations in Chat response. (Web Search) #564

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 61 commits into from
May 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
977a34f
Planning comments
Saraphoo Apr 22, 2025
29c03f0
Planning notes
Saraphoo Apr 22, 2025
d9d0c85
Planning notes
Saraphoo Apr 22, 2025
3776d99
add support for web_search_options
Saraphoo Apr 23, 2025
df9d67e
Add classes for Response Choice web_search_options
Saraphoo Apr 23, 2025
56198b8
web_search_options classes to final classes
Saraphoo Apr 23, 2025
7829a74
Add web_search_options to chat fixtures
Saraphoo Apr 23, 2025
3aa67a4
update docblock and make web_search_option nullable in return
Saraphoo Apr 23, 2025
4c60d4b
expect webSearchOptions
Saraphoo Apr 23, 2025
4b7435a
Return webSearchOptionContent with content size and user location
Saraphoo Apr 23, 2025
f266fd1
Update dockblocks and add type approximate
Saraphoo Apr 23, 2025
e9dc6db
add test for Response Choice WebSearchOptions classes
Saraphoo Apr 23, 2025
b50485e
fix docblock
Saraphoo Apr 23, 2025
e223dd1
Delete package-lock.json
Saraphoo Apr 23, 2025
060e945
Return annotations
Saraphoo Apr 24, 2025
0350362
Test fixtures for annotations
Saraphoo Apr 24, 2025
c3d6d69
Annotations instead of Web_search_options WIP
Saraphoo Apr 24, 2025
05945f0
Remove unneeded class
Saraphoo Apr 24, 2025
d50df1f
Update doc blocks
Saraphoo Apr 24, 2025
7e9ffb7
Update doc block
Saraphoo Apr 24, 2025
6652435
CreateResponseChoiceAnnoations class
Saraphoo Apr 24, 2025
bf46c96
CreateResponseChoiceAnnotaionUrlCitations class
Saraphoo Apr 24, 2025
b380efc
Delete .gitignore
Saraphoo Apr 24, 2025
313798f
git-ignore
Saraphoo Apr 24, 2025
d1e5f06
Restore .gitignore file
Saraphoo Apr 24, 2025
17fa529
test createResponseChoiceAnnotations
Saraphoo Apr 24, 2025
bae6f10
chatCompletionWithAnnotations in correct format
Saraphoo Apr 24, 2025
a34ec91
CreateResponseChoice test in correct format
Saraphoo Apr 24, 2025
8c8befe
better from method and return expected type of url_citation
Saraphoo Apr 24, 2025
8bff115
better testing data for WithAnnotations
Saraphoo Apr 24, 2025
151562a
Test CreateResponseChoiceAnnotationsUrlCitationsTest.php
Saraphoo Apr 24, 2025
0953185
return url_citation items in the correct order
Saraphoo Apr 24, 2025
d1d9350
Pint
Saraphoo Apr 24, 2025
c494f11
Move annotations out of createResponseChoice
Saraphoo Apr 25, 2025
5bbddb1
Move annotations into createResponseMessage
Saraphoo Apr 25, 2025
d1244f2
Annotations in message for chatCompletionWithAnnotations
Saraphoo Apr 25, 2025
32d920e
Update test for CreateResponseChoiceAnnotations to pass
Saraphoo Apr 25, 2025
c2d65dd
CreateResponseChoiceAnnotations returns annotations array
Saraphoo Apr 28, 2025
a0a7ca1
Correct doc block for toArray on choice Annotations
Saraphoo Apr 28, 2025
2b9f0be
WIP collect annotations array for Response Message
Saraphoo Apr 28, 2025
f56e38e
Return annotations from createResponseChoiceAnnotations class to resp…
Saraphoo Apr 28, 2025
5c73592
Correct order for doc block
Saraphoo Apr 28, 2025
37029ee
clean up with pint
Saraphoo Apr 28, 2025
db942f9
expect annotations
Saraphoo Apr 28, 2025
36b1db2
test for web search in messages with url_citations
Saraphoo Apr 28, 2025
df835fe
Just return annotations, no need for filling another array map
Saraphoo Apr 28, 2025
5bc3741
Handle ResponseChoiceAnnotations as object then use toArray method
Saraphoo Apr 29, 2025
c36417c
Retun annotations from class as expected
Saraphoo Apr 29, 2025
639a7a0
Correct docblocks
Saraphoo Apr 29, 2025
281ae33
check urlCitations in createResponseChoiceAnnotaitons
Saraphoo Apr 29, 2025
92c86d1
point to correct item in array for test
Saraphoo Apr 29, 2025
5c7cf51
pint
Saraphoo Apr 29, 2025
e3c77ad
Correct doc block with single quotes for url_citations
Saraphoo Apr 29, 2025
e5afedf
doc blocks with single quotes for url_citation
Saraphoo Apr 29, 2025
a0c09e6
Remove array filter on array map for urlCitations
Saraphoo Apr 29, 2025
4502386
Assert toArray with function call
Saraphoo Apr 30, 2025
853d7ab
Annotations treated as arrays on message object
Saraphoo Apr 30, 2025
8f8085a
WIP Type fix with PHPstan
Saraphoo Apr 30, 2025
3763ac8
Fix doc blocks
Saraphoo Apr 30, 2025
4924a07
fix doc block
Saraphoo Apr 30, 2025
48e247f
test: augment tests for annotations
iBotPeaches May 1, 2025
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
6 changes: 3 additions & 3 deletions src/Responses/Chat/CreateResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@
use OpenAI\Testing\Responses\Concerns\Fakeable;

/**
* @implements ResponseContract<array{id?: string, object: string, created: int, model: string, system_fingerprint?: string, choices: array<int, array{index: int, message: array{role: string, content: string|null, function_call?: array{name: string, arguments: string}, tool_calls?: array<int, array{id: string, type: string, function: array{name: string, arguments: string}}>}, logprobs: ?array{content: ?array<int, array{token: string, logprob: float, bytes: ?array<int, int>}>}, finish_reason: string|null}>, usage?: array{prompt_tokens: int, completion_tokens: int|null, total_tokens: int}}>
* @implements ResponseContract<array{id?: string, object: string, created: int, model: string, system_fingerprint?: string, choices: array<int, array{index: int, message: array{role: string, content: string|null, annotations?: array<int, array{type: string, url_citation: array{start_index: int, end_index: int, title: string, url: string}}>, function_call?: array{name: string, arguments: string}, tool_calls?: array<int, array{id: string, type: string, function: array{name: string, arguments: string}}>}, logprobs: ?array{content: ?array<int, array{token: string, logprob: float, bytes: ?array<int, int>}>}, finish_reason: string|null}>, usage?: array{prompt_tokens: int, completion_tokens: int|null, total_tokens: int}}>
*/
final class CreateResponse implements ResponseContract, ResponseHasMetaInformationContract
{
/**
* @use ArrayAccessible<array{id: string, object: string, created: int, model: string, system_fingerprint?: string, choices: array<int, array{index: int, message: array{role: string, content: string|null, function_call?: array{name: string, arguments: string}, tool_calls?: array<int, array{id: string, type: string, function: array{name: string, arguments: string}}>}, logprobs: ?array{content: ?array<int, array{token: string, logprob: float, bytes: ?array<int, int>}>}, finish_reason: string|null}>, usage: array{prompt_tokens: int, completion_tokens: int|null, total_tokens: int}}>
* @use ArrayAccessible<array{id: string, object: string, created: int, model: string, system_fingerprint?: string, choices: array<int, array{index: int, message: array{role: string, content: string|null, annotations?: array<int, array{type: string, url_citation: array{start_index: int, end_index: int, title: string, url: string}}>, function_call?: array{name: string, arguments: string}, tool_calls?: array<int, array{id: string, type: string, function: array{name: string, arguments: string}}>}, logprobs: ?array{content: ?array<int, array{token: string, logprob: float, bytes: ?array<int, int>}>}, finish_reason: string|null}>, usage: array{prompt_tokens: int, completion_tokens: int|null, total_tokens: int}}>
*/
use ArrayAccessible;

Expand All @@ -41,7 +41,7 @@ private function __construct(
/**
* Acts as static factory, and returns a new Response instance.
*
* @param array{id?: string, object: string, created: int, model: string, system_fingerprint?: string, choices: array<int, array{index: int, message: array{role: string, content: ?string, function_call: ?array{name: string, arguments: string}, tool_calls: ?array<int, array{id: string, type: string, function: array{name: string, arguments: string}}>}, logprobs: ?array{content: ?array<int, array{token: string, logprob: float, bytes: ?array<int, int>}>}, finish_reason: string|null}>, usage?: array{prompt_tokens: int, completion_tokens: int|null, total_tokens: int, prompt_tokens_details?:array{cached_tokens:int}, completion_tokens_details?:array{audio_tokens?:int, reasoning_tokens:int, accepted_prediction_tokens:int, rejected_prediction_tokens:int}}} $attributes
* @param array{id?: string, object: string, created: int, model: string, system_fingerprint?: string, choices: array<int, array{index: int, message: array{role: string, content: ?string, annotations?: array<int, array{type: string, url_citation: array{start_index: int, end_index: int, title: string, url: string}}>, function_call: ?array{name: string, arguments: string}, tool_calls: ?array<int, array{id: string, type: string, function: array{name: string, arguments: string}}>}, logprobs: ?array{content: ?array<int, array{token: string, logprob: float, bytes: ?array<int, int>}>}, finish_reason: string|null}>, usage?: array{prompt_tokens: int, completion_tokens: int|null, total_tokens: int, prompt_tokens_details?:array{cached_tokens:int}, completion_tokens_details?:array{audio_tokens?:int, reasoning_tokens:int, accepted_prediction_tokens:int, rejected_prediction_tokens:int}}} $attributes
*/
public static function from(array $attributes, MetaInformation $meta): self
{
Expand Down
4 changes: 2 additions & 2 deletions src/Responses/Chat/CreateResponseChoice.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ private function __construct(
) {}

/**
* @param array{index: int, message: array{role: string, content: ?string, function_call: ?array{name: string, arguments: string}, tool_calls: ?array<int, array{id: string, type: string, function: array{name: string, arguments: string}}>}, logprobs?: ?array{content: ?array<int, array{token: string, logprob: float, bytes: ?array<int, int>}>}, finish_reason: string|null} $attributes
* @param array{index: int, message: array{role: string, content: ?string, annotations?: array<int, array{type: string, url_citation: array{start_index: int, end_index: int, title: string, url: string}}>, function_call: ?array{name: string, arguments: string}, tool_calls: ?array<int, array{id: string, type: string, function: array{name: string, arguments: string}}>} ,logprobs?: ?array{content: ?array<int, array{token: string, logprob: float, bytes: ?array<int, int>}>}, finish_reason: string|null} $attributes
*/
public static function from(array $attributes): self
{
Expand All @@ -27,7 +27,7 @@ public static function from(array $attributes): self
}

/**
* @return array{index: int, message: array{role: string, content: string|null, function_call?: array{name: string, arguments: string}, tool_calls?: array<int, array{id: string, type: string, function: array{name: string, arguments: string}}>}, logprobs: ?array{content: ?array<int, array{token: string, logprob: float, bytes: ?array<int, int>}>}, finish_reason: string|null}
* @return array{index: int, message: array{role: string, content: string|null, annotations?: array<int, array{type: string, url_citation: array{start_index: int, end_index: int, title: string, url: string}}>, function_call?: array{name: string, arguments: string}, tool_calls?: array<int, array{id: string, type: string, function: array{name: string, arguments: string}}>}, logprobs: ?array{content: ?array<int, array{token: string, logprob: float, bytes: ?array<int, int>}>}, finish_reason: string|null}
*/
public function toArray(): array
{
Expand Down
33 changes: 33 additions & 0 deletions src/Responses/Chat/CreateResponseChoiceAnnotations.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace OpenAI\Responses\Chat;

final class CreateResponseChoiceAnnotations
{
public function __construct(
public readonly string $type,
public readonly CreateResponseChoiceAnnotationsUrlCitations $urlCitations
) {}

/**
* @param array{type: string, url_citation: array{end_index: int, start_index: int, title: string, url: string}} $attributes
*/
public static function from(array $attributes): self
{
return new self(
$attributes['type'],
CreateResponseChoiceAnnotationsUrlCitations::from($attributes['url_citation'])
);
}

/**
* @return array{type: string, url_citation: array{end_index: int, start_index: int, title: string, url: string}}
*/
public function toArray(): array
{
return [
'type' => $this->type,
'url_citation' => $this->urlCitations->toArray(),
];
}
}
39 changes: 39 additions & 0 deletions src/Responses/Chat/CreateResponseChoiceAnnotationsUrlCitations.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace OpenAI\Responses\Chat;

final class CreateResponseChoiceAnnotationsUrlCitations
{
public function __construct(
public readonly int $endIndex,
public readonly int $startIndex,
public readonly string $title,
public readonly string $url,
) {}

/**
* @param array{end_index: int, start_index: int, title: string, url: string} $attributes
*/
public static function from(array $attributes): self
{
return new self(
$attributes['end_index'],
$attributes['start_index'],
$attributes['title'],
$attributes['url'],
);
}

/**
* @return array{end_index: int, start_index: int, title: string, url: string}
*/
public function toArray(): array
{
return [
'end_index' => $this->endIndex,
'start_index' => $this->startIndex,
'title' => $this->title,
'url' => $this->url,
];
}
}
15 changes: 13 additions & 2 deletions src/Responses/Chat/CreateResponseMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,40 @@ final class CreateResponseMessage
{
/**
* @param array<int, CreateResponseToolCall> $toolCalls
* @param array<int, CreateResponseChoiceAnnotations> $annotations
*/
private function __construct(
public readonly string $role,
public readonly ?string $content,
public readonly array $annotations,
public readonly array $toolCalls,
public readonly ?CreateResponseFunctionCall $functionCall,
) {}

/**
* @param array{role: string, content: ?string, function_call: ?array{name: string, arguments: string}, tool_calls: ?array<int, array{id: string, type: string, function: array{name: string, arguments: string}}>} $attributes
* @param array{role: string, content: ?string, annotations?: array<int, array{type: string, url_citation: array{start_index: int, end_index: int, title: string, url: string}}>, function_call: ?array{name: string, arguments: string}, tool_calls: ?array<int, array{id: string, type: string, function: array{name: string, arguments: string}}>} $attributes
*/
public static function from(array $attributes): self
{
$toolCalls = array_map(fn (array $result): CreateResponseToolCall => CreateResponseToolCall::from(
$result
), $attributes['tool_calls'] ?? []);

$annotations = array_map(fn (array $result): CreateResponseChoiceAnnotations => CreateResponseChoiceAnnotations::from(
$result,
), $attributes['annotations'] ?? []);

return new self(
$attributes['role'],
$attributes['content'] ?? null,
$annotations,
$toolCalls,
isset($attributes['function_call']) ? CreateResponseFunctionCall::from($attributes['function_call']) : null,
);
}

/**
* @return array{role: string, content: string|null, function_call?: array{name: string, arguments: string}, tool_calls?: array<int, array{id: string, type: string, function: array{name: string, arguments: string}}>}
* @return array{role: string, content: string|null, annotations?: array<int, array{type: string, url_citation: array{start_index: int, end_index: int, title: string, url: string}}>, function_call?: array{name: string, arguments: string}, tool_calls?: array<int, array{id: string, type: string, function: array{name: string, arguments: string}}>}
*/
public function toArray(): array
{
Expand All @@ -43,6 +50,10 @@ public function toArray(): array
'content' => $this->content,
];

if ($this->annotations !== []) {
$data['annotations'] = array_map(fn (CreateResponseChoiceAnnotations $annotations): array => $annotations->toArray(), $this->annotations);
}

if ($this->functionCall instanceof CreateResponseFunctionCall) {
$data['function_call'] = $this->functionCall->toArray();
}
Expand Down
34 changes: 34 additions & 0 deletions tests/Fixtures/Chat.php
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,40 @@ function chatCompletionWithoutUsage(): array
];
}

/**
* @return array<string, mixed>
*/
function chatCompletionWithAnnotations(): array
{
return [
'id' => 'chatcmpl-123',
'object' => 'chat.completion',
'created' => 1677652288,
'model' => 'gpt-4o-mini-search-preview',
'choices' => [
[
'index' => 0,
'message' => [
'role' => 'assistant',
'content' => 'Hello World',
'annotations' => [
[
'type' => 'url_citation',
'url_citation' => [
'end_index' => 5,
'start_index' => 0,
'title' => 'Hello',
'url' => 'https://example.com',
],
],
],
],
'finish_reason' => 'stop',
],
],
];
}

/**
* @return array<string, mixed>
*/
Expand Down
19 changes: 19 additions & 0 deletions tests/Responses/Chat/CreateResponseChoiceAnnotations.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

use OpenAI\Responses\Chat\CreateResponseChoiceAnnotations;
use OpenAI\Responses\Chat\CreateResponseChoiceAnnotationsUrlCitations;

it('from url_citation annotation', function () {
$result = CreateResponseChoiceAnnotations::from(chatCompletionWithAnnotations()['choices'][0]['message']['annotations'][0]);

expect($result)
->type->toBe('url_citation')
->urlCitations->toBeInstanceOf(CreateResponseChoiceAnnotationsUrlCitations::class);
});

test('to array', function () {
$result = CreateResponseChoiceAnnotations::from(chatCompletionWithAnnotations()['choices'][0]['message']['annotations'][0]);

expect($result->toArray())
->toBe(chatCompletionWithAnnotations()['choices'][0]['message']['annotations'][0]);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

use OpenAI\Responses\Chat\CreateResponseChoiceAnnotationsUrlCitations;

it('from', function () {
$result = CreateResponseChoiceAnnotationsUrlCitations::from(chatCompletionWithAnnotations()['choices'][0]['message']['annotations'][0]['url_citation']);

expect($result)
->endIndex->toBe(5)
->startIndex->toBe(0)
->title->toBe('Hello')
->url->toBe('https://example.com');
});

test('to array', function () {
$result = CreateResponseChoiceAnnotationsUrlCitations::from(chatCompletionWithAnnotations()['choices'][0]['message']['annotations'][0]['url_citation']);

expect($result->toArray())
->toBe(chatCompletionWithAnnotations()['choices'][0]['message']['annotations'][0]['url_citation']);
});
21 changes: 21 additions & 0 deletions tests/Responses/Chat/CreateResponseMessage.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use OpenAI\Responses\Chat\CreateResponseChoiceAnnotations;
use OpenAI\Responses\Chat\CreateResponseFunctionCall;
use OpenAI\Responses\Chat\CreateResponseMessage;
use OpenAI\Responses\Chat\CreateResponseToolCall;
Expand All @@ -10,6 +11,7 @@
expect($result)
->role->toBe('assistant')
->content->toBe("\n\nHello there, how may I assist you today?")
->annotations->toBeArray()
->functionCall->toBeNull();
});

Expand All @@ -19,6 +21,7 @@
expect($result)
->role->toBe('assistant')
->content->toBeNull()
->annotations->toBeArray()
->functionCall->toBeInstanceOf(CreateResponseFunctionCall::class);
});

Expand All @@ -33,6 +36,17 @@
->toolCalls->each->toBeInstanceOf(CreateResponseToolCall::class);
});

test('from annotations response', function () {
$result = CreateResponseMessage::from(chatCompletionWithAnnotations()['choices'][0]['message']);

expect($result)
->role->toBe('assistant')
->content->toBe('Hello World')
->annotations->toBeArray()
->annotations->toHaveCount(1)
->annotations->each->toBeInstanceOf(CreateResponseChoiceAnnotations::class);
});

test('from function response without content', function () {
$result = CreateResponseMessage::from(chatCompletionMessageWithFunctionAndNoContent());

Expand Down Expand Up @@ -62,3 +76,10 @@
expect($result->toArray())
->toBe(chatCompletionWithToolCalls()['choices'][0]['message']);
});

test('to array from annotations response', function () {
$result = CreateResponseMessage::from(chatCompletionWithAnnotations()['choices'][0]['message']);

expect($result->toArray())
->toBe(chatCompletionWithAnnotations()['choices'][0]['message']);
});