Skip to content

Commit 9bd5a25

Browse files
authored
feat(OpenAI): Support annotations in Chat response. (Web Search) (#564)
* Planning comments * Planning notes * Planning notes * add support for web_search_options * Add classes for Response Choice web_search_options * web_search_options classes to final classes * Add web_search_options to chat fixtures * update docblock and make web_search_option nullable in return * expect webSearchOptions * Return webSearchOptionContent with content size and user location * Update dockblocks and add type approximate * add test for Response Choice WebSearchOptions classes * fix docblock * Delete package-lock.json * Return annotations * Test fixtures for annotations * Annotations instead of Web_search_options WIP * Remove unneeded class * Update doc blocks * Update doc block * CreateResponseChoiceAnnoations class * CreateResponseChoiceAnnotaionUrlCitations class * Delete .gitignore * git-ignore * Restore .gitignore file * test createResponseChoiceAnnotations * chatCompletionWithAnnotations in correct format * CreateResponseChoice test in correct format * better from method and return expected type of url_citation * better testing data for WithAnnotations * Test CreateResponseChoiceAnnotationsUrlCitationsTest.php * return url_citation items in the correct order * Pint * Move annotations out of createResponseChoice * Move annotations into createResponseMessage * Annotations in message for chatCompletionWithAnnotations * Update test for CreateResponseChoiceAnnotations to pass * CreateResponseChoiceAnnotations returns annotations array * Correct doc block for toArray on choice Annotations * WIP collect annotations array for Response Message * Return annotations from createResponseChoiceAnnotations class to response message correctly * Correct order for doc block * clean up with pint * expect annotations * test for web search in messages with url_citations * Just return annotations, no need for filling another array map * Handle ResponseChoiceAnnotations as object then use toArray method * Retun annotations from class as expected * Correct docblocks * check urlCitations in createResponseChoiceAnnotaitons * point to correct item in array for test * pint * Correct doc block with single quotes for url_citations * doc blocks with single quotes for url_citation * Remove array filter on array map for urlCitations * Assert toArray with function call * Annotations treated as arrays on message object * WIP Type fix with PHPstan * Fix doc blocks * fix doc block * test: augment tests for annotations
1 parent 62e9822 commit 9bd5a25

9 files changed

+184
-7
lines changed

src/Responses/Chat/CreateResponse.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212
use OpenAI\Testing\Responses\Concerns\Fakeable;
1313

1414
/**
15-
* @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}}>
15+
* @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}}>
1616
*/
1717
final class CreateResponse implements ResponseContract, ResponseHasMetaInformationContract
1818
{
1919
/**
20-
* @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}}>
20+
* @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}}>
2121
*/
2222
use ArrayAccessible;
2323

@@ -41,7 +41,7 @@ private function __construct(
4141
/**
4242
* Acts as static factory, and returns a new Response instance.
4343
*
44-
* @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
44+
* @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
4545
*/
4646
public static function from(array $attributes, MetaInformation $meta): self
4747
{

src/Responses/Chat/CreateResponseChoice.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ private function __construct(
1414
) {}
1515

1616
/**
17-
* @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
17+
* @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
1818
*/
1919
public static function from(array $attributes): self
2020
{
@@ -27,7 +27,7 @@ public static function from(array $attributes): self
2727
}
2828

2929
/**
30-
* @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}
30+
* @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}
3131
*/
3232
public function toArray(): array
3333
{
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace OpenAI\Responses\Chat;
4+
5+
final class CreateResponseChoiceAnnotations
6+
{
7+
public function __construct(
8+
public readonly string $type,
9+
public readonly CreateResponseChoiceAnnotationsUrlCitations $urlCitations
10+
) {}
11+
12+
/**
13+
* @param array{type: string, url_citation: array{end_index: int, start_index: int, title: string, url: string}} $attributes
14+
*/
15+
public static function from(array $attributes): self
16+
{
17+
return new self(
18+
$attributes['type'],
19+
CreateResponseChoiceAnnotationsUrlCitations::from($attributes['url_citation'])
20+
);
21+
}
22+
23+
/**
24+
* @return array{type: string, url_citation: array{end_index: int, start_index: int, title: string, url: string}}
25+
*/
26+
public function toArray(): array
27+
{
28+
return [
29+
'type' => $this->type,
30+
'url_citation' => $this->urlCitations->toArray(),
31+
];
32+
}
33+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace OpenAI\Responses\Chat;
4+
5+
final class CreateResponseChoiceAnnotationsUrlCitations
6+
{
7+
public function __construct(
8+
public readonly int $endIndex,
9+
public readonly int $startIndex,
10+
public readonly string $title,
11+
public readonly string $url,
12+
) {}
13+
14+
/**
15+
* @param array{end_index: int, start_index: int, title: string, url: string} $attributes
16+
*/
17+
public static function from(array $attributes): self
18+
{
19+
return new self(
20+
$attributes['end_index'],
21+
$attributes['start_index'],
22+
$attributes['title'],
23+
$attributes['url'],
24+
);
25+
}
26+
27+
/**
28+
* @return array{end_index: int, start_index: int, title: string, url: string}
29+
*/
30+
public function toArray(): array
31+
{
32+
return [
33+
'end_index' => $this->endIndex,
34+
'start_index' => $this->startIndex,
35+
'title' => $this->title,
36+
'url' => $this->url,
37+
];
38+
}
39+
}

src/Responses/Chat/CreateResponseMessage.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,33 +8,40 @@ final class CreateResponseMessage
88
{
99
/**
1010
* @param array<int, CreateResponseToolCall> $toolCalls
11+
* @param array<int, CreateResponseChoiceAnnotations> $annotations
1112
*/
1213
private function __construct(
1314
public readonly string $role,
1415
public readonly ?string $content,
16+
public readonly array $annotations,
1517
public readonly array $toolCalls,
1618
public readonly ?CreateResponseFunctionCall $functionCall,
1719
) {}
1820

1921
/**
20-
* @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
22+
* @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
2123
*/
2224
public static function from(array $attributes): self
2325
{
2426
$toolCalls = array_map(fn (array $result): CreateResponseToolCall => CreateResponseToolCall::from(
2527
$result
2628
), $attributes['tool_calls'] ?? []);
2729

30+
$annotations = array_map(fn (array $result): CreateResponseChoiceAnnotations => CreateResponseChoiceAnnotations::from(
31+
$result,
32+
), $attributes['annotations'] ?? []);
33+
2834
return new self(
2935
$attributes['role'],
3036
$attributes['content'] ?? null,
37+
$annotations,
3138
$toolCalls,
3239
isset($attributes['function_call']) ? CreateResponseFunctionCall::from($attributes['function_call']) : null,
3340
);
3441
}
3542

3643
/**
37-
* @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}}>}
44+
* @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}}>}
3845
*/
3946
public function toArray(): array
4047
{
@@ -43,6 +50,10 @@ public function toArray(): array
4350
'content' => $this->content,
4451
];
4552

53+
if ($this->annotations !== []) {
54+
$data['annotations'] = array_map(fn (CreateResponseChoiceAnnotations $annotations): array => $annotations->toArray(), $this->annotations);
55+
}
56+
4657
if ($this->functionCall instanceof CreateResponseFunctionCall) {
4758
$data['function_call'] = $this->functionCall->toArray();
4859
}

tests/Fixtures/Chat.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,40 @@ function chatCompletionWithoutUsage(): array
237237
];
238238
}
239239

240+
/**
241+
* @return array<string, mixed>
242+
*/
243+
function chatCompletionWithAnnotations(): array
244+
{
245+
return [
246+
'id' => 'chatcmpl-123',
247+
'object' => 'chat.completion',
248+
'created' => 1677652288,
249+
'model' => 'gpt-4o-mini-search-preview',
250+
'choices' => [
251+
[
252+
'index' => 0,
253+
'message' => [
254+
'role' => 'assistant',
255+
'content' => 'Hello World',
256+
'annotations' => [
257+
[
258+
'type' => 'url_citation',
259+
'url_citation' => [
260+
'end_index' => 5,
261+
'start_index' => 0,
262+
'title' => 'Hello',
263+
'url' => 'https://example.com',
264+
],
265+
],
266+
],
267+
],
268+
'finish_reason' => 'stop',
269+
],
270+
],
271+
];
272+
}
273+
240274
/**
241275
* @return array<string, mixed>
242276
*/
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
use OpenAI\Responses\Chat\CreateResponseChoiceAnnotations;
4+
use OpenAI\Responses\Chat\CreateResponseChoiceAnnotationsUrlCitations;
5+
6+
it('from url_citation annotation', function () {
7+
$result = CreateResponseChoiceAnnotations::from(chatCompletionWithAnnotations()['choices'][0]['message']['annotations'][0]);
8+
9+
expect($result)
10+
->type->toBe('url_citation')
11+
->urlCitations->toBeInstanceOf(CreateResponseChoiceAnnotationsUrlCitations::class);
12+
});
13+
14+
test('to array', function () {
15+
$result = CreateResponseChoiceAnnotations::from(chatCompletionWithAnnotations()['choices'][0]['message']['annotations'][0]);
16+
17+
expect($result->toArray())
18+
->toBe(chatCompletionWithAnnotations()['choices'][0]['message']['annotations'][0]);
19+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
use OpenAI\Responses\Chat\CreateResponseChoiceAnnotationsUrlCitations;
4+
5+
it('from', function () {
6+
$result = CreateResponseChoiceAnnotationsUrlCitations::from(chatCompletionWithAnnotations()['choices'][0]['message']['annotations'][0]['url_citation']);
7+
8+
expect($result)
9+
->endIndex->toBe(5)
10+
->startIndex->toBe(0)
11+
->title->toBe('Hello')
12+
->url->toBe('https://example.com');
13+
});
14+
15+
test('to array', function () {
16+
$result = CreateResponseChoiceAnnotationsUrlCitations::from(chatCompletionWithAnnotations()['choices'][0]['message']['annotations'][0]['url_citation']);
17+
18+
expect($result->toArray())
19+
->toBe(chatCompletionWithAnnotations()['choices'][0]['message']['annotations'][0]['url_citation']);
20+
});

tests/Responses/Chat/CreateResponseMessage.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php
22

3+
use OpenAI\Responses\Chat\CreateResponseChoiceAnnotations;
34
use OpenAI\Responses\Chat\CreateResponseFunctionCall;
45
use OpenAI\Responses\Chat\CreateResponseMessage;
56
use OpenAI\Responses\Chat\CreateResponseToolCall;
@@ -10,6 +11,7 @@
1011
expect($result)
1112
->role->toBe('assistant')
1213
->content->toBe("\n\nHello there, how may I assist you today?")
14+
->annotations->toBeArray()
1315
->functionCall->toBeNull();
1416
});
1517

@@ -19,6 +21,7 @@
1921
expect($result)
2022
->role->toBe('assistant')
2123
->content->toBeNull()
24+
->annotations->toBeArray()
2225
->functionCall->toBeInstanceOf(CreateResponseFunctionCall::class);
2326
});
2427

@@ -33,6 +36,17 @@
3336
->toolCalls->each->toBeInstanceOf(CreateResponseToolCall::class);
3437
});
3538

39+
test('from annotations response', function () {
40+
$result = CreateResponseMessage::from(chatCompletionWithAnnotations()['choices'][0]['message']);
41+
42+
expect($result)
43+
->role->toBe('assistant')
44+
->content->toBe('Hello World')
45+
->annotations->toBeArray()
46+
->annotations->toHaveCount(1)
47+
->annotations->each->toBeInstanceOf(CreateResponseChoiceAnnotations::class);
48+
});
49+
3650
test('from function response without content', function () {
3751
$result = CreateResponseMessage::from(chatCompletionMessageWithFunctionAndNoContent());
3852

@@ -62,3 +76,10 @@
6276
expect($result->toArray())
6377
->toBe(chatCompletionWithToolCalls()['choices'][0]['message']);
6478
});
79+
80+
test('to array from annotations response', function () {
81+
$result = CreateResponseMessage::from(chatCompletionWithAnnotations()['choices'][0]['message']);
82+
83+
expect($result->toArray())
84+
->toBe(chatCompletionWithAnnotations()['choices'][0]['message']);
85+
});

0 commit comments

Comments
 (0)