Skip to content

Commit 62e9822

Browse files
feat(OpenAI): Add category applied input types to moderation response (openai-php#572)
* Add support for multi-modal moderation inputs and category applied input types * put parameter documentation in single line * Add support for omni moderation with text and image inputs * put DocBlock in single line * chore: remove unneeded docblock changes * chore: unneeded spacing change * chore: pint --------- Co-authored-by: Connor Tumbleson <[email protected]>
1 parent d7e3238 commit 62e9822

File tree

7 files changed

+204
-11
lines changed

7 files changed

+204
-11
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenAI\Enums\Moderations;
6+
7+
enum CategoryAppliedInputType: string
8+
{
9+
case Text = 'text';
10+
case Image = 'image';
11+
case Audio = 'audio';
12+
}

src/Resources/Moderations.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public function create(array $parameters): CreateResponse
2424
{
2525
$payload = Payload::create('moderations', $parameters);
2626

27-
/** @var Response<array{id: string, model: string, results: array<int, array{categories: array<string, bool>, category_scores: array<string, float>, flagged: bool}>}> $response */
27+
/** @var Response<array{id: string, model: string, results: array<int, array{categories: array<string, bool>, category_scores: array<string, float>, flagged: bool,category_applied_input_types?: array<string, array<int, string>>}>}> $response */
2828
$response = $this->transporter->requestObject($payload);
2929

3030
return CreateResponse::from($response->data(), $response->meta());

src/Responses/Moderations/CreateResponse.php

+3-3
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, model: string, results: array<int, array{categories: array<string, bool>, category_scores: array<string, float>, flagged: bool}>}>
15+
* @implements ResponseContract<array{id: string, model: string, results: array<int, array{categories: array<string, bool>, category_scores: array<string, float>, flagged: bool, category_applied_input_types?: array<string, array<int, string>>}>}>
1616
*/
1717
final class CreateResponse implements ResponseContract, ResponseHasMetaInformationContract
1818
{
1919
/**
20-
* @use ArrayAccessible<array{id: string, model: string, results: array<int, array{categories: array<string, bool>, category_scores: array<string, float>, flagged: bool}>}>
20+
* @use ArrayAccessible<array{id: string, model: string, results: array<int, array{categories: array<string, bool>, category_scores: array<string, float>, flagged: bool, category_applied_input_types?: array<string, array<int, string>>}>}>
2121
*/
2222
use ArrayAccessible;
2323

@@ -37,7 +37,7 @@ private function __construct(
3737
/**
3838
* Acts as static factory, and returns a new Response instance.
3939
*
40-
* @param array{id: string, model: string, results: array<int, array{categories: array<string, bool>, category_scores: array<string, float>, flagged: bool}>} $attributes
40+
* @param array{id: string, model: string, results: array<int, array{categories: array<string, bool>, category_scores: array<string, float>, flagged: bool, category_applied_input_types?: array<string, array<int, string>>}>} $attributes
4141
*/
4242
public static function from(array $attributes, MetaInformation $meta): self
4343
{

src/Responses/Moderations/CreateResponseResult.php

+13-4
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@ final class CreateResponseResult
1010
{
1111
/**
1212
* @param array<string, CreateResponseCategory> $categories
13+
* @param array<string, array<string>> $categoryAppliedInputTypes
1314
*/
1415
private function __construct(
1516
public readonly array $categories,
1617
public readonly bool $flagged,
18+
public readonly ?array $categoryAppliedInputTypes,
1719
) {
1820
// ..
1921
}
2022

2123
/**
22-
* @param array{categories: array<string, bool>, category_scores: array<string, float>, flagged: bool} $attributes
24+
* @param array{categories: array<string, bool>, category_scores: array<string, float>, flagged: bool, category_applied_input_types?: array<string, array<int, string>>} $attributes
2325
*/
2426
public static function from(array $attributes): self
2527
{
@@ -40,12 +42,13 @@ public static function from(array $attributes): self
4042

4143
return new CreateResponseResult(
4244
$categories,
43-
$attributes['flagged']
45+
$attributes['flagged'],
46+
$attributes['category_applied_input_types'] ?? null,
4447
);
4548
}
4649

4750
/**
48-
* @return array{categories: array<string, bool>, category_scores: array<string, float>, flagged: bool}
51+
* @return array{ categories: array<string, bool>, category_scores: array<string, float>, flagged: bool, category_applied_input_types?: array<string, array<int, string>>}
4952
*/
5053
public function toArray(): array
5154
{
@@ -56,10 +59,16 @@ public function toArray(): array
5659
$categoryScores[$category->category->value] = $category->score;
5760
}
5861

59-
return [
62+
$result = [
6063
'categories' => $categories,
6164
'category_scores' => $categoryScores,
6265
'flagged' => $this->flagged,
6366
];
67+
68+
if ($this->categoryAppliedInputTypes !== null) {
69+
$result['category_applied_input_types'] = $this->categoryAppliedInputTypes;
70+
}
71+
72+
return $result;
6473
}
6574
}

tests/Fixtures/Moderation.php

+76
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,82 @@ function moderationOmniResource(): array
8383
'violence/graphic' => 0.036865197122097015,
8484
],
8585
'flagged' => true,
86+
'category_applied_input_types' => [
87+
'hate' => ['text'],
88+
'hate/threatening' => ['text'],
89+
'harassment' => ['text'],
90+
'harassment/threatening' => ['text'],
91+
'self-harm' => ['text'],
92+
'self-harm/intent' => ['text'],
93+
'self-harm/instructions' => ['text'],
94+
'sexual' => ['text'],
95+
'sexual/minors' => ['text'],
96+
'violence' => ['text'],
97+
'violence/graphic' => ['text'],
98+
'illicit' => ['text'],
99+
'illicit/violent' => ['text'],
100+
],
101+
],
102+
],
103+
];
104+
}
105+
106+
/**
107+
* @return array<string, mixed>
108+
*/
109+
function moderationOmniWithTextAndImageResource(): array
110+
{
111+
return [
112+
'id' => 'modr-5MWoLO',
113+
'model' => 'omni-moderation-001',
114+
'results' => [
115+
[
116+
'categories' => [
117+
'hate' => false,
118+
'hate/threatening' => false,
119+
'harassment' => false,
120+
'harassment/threatening' => false,
121+
'illicit' => false,
122+
'illicit/violent' => false,
123+
'self-harm' => false,
124+
'self-harm/intent' => false,
125+
'self-harm/instructions' => false,
126+
'sexual' => false,
127+
'sexual/minors' => false,
128+
'violence' => true,
129+
'violence/graphic' => true,
130+
],
131+
'category_scores' => [
132+
'hate' => 0.22714105248451233,
133+
'hate/threatening' => 0.4132447838783264,
134+
'illicit' => 0.1602763684674149,
135+
'illicit/violent' => 0.9223177433013916,
136+
'harassment' => 0.1602763684674149,
137+
'harassment/threatening' => 0.1602763684674149,
138+
'self-harm' => 0.005232391878962517,
139+
'self-harm/intent' => 0.005134391873962517,
140+
'self-harm/instructions' => 0.005132591874962517,
141+
'sexual' => 0.01407341007143259,
142+
'sexual/minors' => 0.0038522258400917053,
143+
'violence' => 0.4132447838783264,
144+
'violence/graphic' => 5.7929166992142E-5,
145+
],
146+
'flagged' => true,
147+
'category_applied_input_types' => [
148+
'hate' => ['text'],
149+
'hate/threatening' => ['text'],
150+
'harassment' => ['text'],
151+
'harassment/threatening' => ['text'],
152+
'self-harm' => ['text', 'image'],
153+
'self-harm/intent' => ['text', 'image'],
154+
'self-harm/instructions' => ['text', 'image'],
155+
'sexual' => ['text', 'image'],
156+
'sexual/minors' => ['text', 'image'],
157+
'violence' => ['text', 'image'],
158+
'violence/graphic' => ['text', 'image'],
159+
'illicit' => ['text'],
160+
'illicit/violent' => ['text'],
161+
],
86162
],
87163
],
88164
];

tests/Resources/Moderations.php

+68-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
<?php
22

33
use OpenAI\Enums\Moderations\Category;
4+
use OpenAI\Enums\Moderations\CategoryAppliedInputType;
45
use OpenAI\Responses\Meta\MetaInformation;
56
use OpenAI\Responses\Moderations\CreateResponse;
67
use OpenAI\Responses\Moderations\CreateResponseCategory;
78
use OpenAI\Responses\Moderations\CreateResponseResult;
89
use OpenAI\ValueObjects\Transporter\Response;
910

11+
dataset('create omni inputs', [
12+
'text_in_array' => [
13+
['type' => 'text', 'text' => 'I love to kill...'],
14+
],
15+
'basic_text' => 'I want to kill them.',
16+
]);
17+
1018
test('create legacy', closure: function () {
1119
$client = mockClient('POST', 'moderations', [
1220
'model' => 'text-moderation-latest',
@@ -44,15 +52,15 @@
4452
->toBeInstanceOf(MetaInformation::class);
4553
});
4654

47-
test('create omni', closure: function () {
55+
test('create omni', closure: function ($input) {
4856
$client = mockClient('POST', 'moderations', [
4957
'model' => 'omni-moderation-latest',
50-
'input' => 'I want to kill them.',
58+
'input' => $input,
5159
], Response::from(moderationOmniResource(), metaHeaders()));
5260

5361
$result = $client->moderations()->create([
5462
'model' => 'omni-moderation-latest',
55-
'input' => 'I want to kill them.',
63+
'input' => $input,
5664
]);
5765

5866
expect($result)
@@ -77,6 +85,63 @@
7785
->violated->toBe(true)
7886
->score->toBe(0.9223177433013916);
7987

88+
expect($result->results[0]->categoryAppliedInputTypes)
89+
->toHaveCount(13)
90+
->each->toBe([CategoryAppliedInputType::Text->value]);
91+
8092
expect($result->meta())
8193
->toBeInstanceOf(MetaInformation::class);
94+
})->with('create omni inputs');
95+
96+
test('create omni with image and text', closure: function () {
97+
$client = mockClient('POST', 'moderations', [
98+
'model' => 'omni-moderation-latest',
99+
'input' => [
100+
['type' => 'text', 'text' => '.. I want to kill...'],
101+
[
102+
'type' => 'image_url',
103+
'image_url' => [
104+
'url' => 'https://example.com/image.png',
105+
],
106+
],
107+
],
108+
], Response::from(moderationOmniWithTextAndImageResource(), metaHeaders()));
109+
110+
$result = $client->moderations()->create([
111+
'model' => 'omni-moderation-latest',
112+
'input' => [
113+
['type' => 'text', 'text' => '.. I want to kill...'],
114+
[
115+
'type' => 'image_url',
116+
'image_url' => [
117+
'url' => 'https://example.com/image.png',
118+
],
119+
],
120+
],
121+
]);
122+
123+
expect($result)
124+
->toBeInstanceOf(CreateResponse::class)
125+
->id->toBe('modr-5MWoLO')
126+
->model->toBe('omni-moderation-001')
127+
->results->toBeArray()->toHaveCount(1)
128+
->results->each->toBeInstanceOf(CreateResponseResult::class);
129+
130+
expect($result->results[0])
131+
->flagged->toBeTrue()
132+
->categories->toHaveCount(13)
133+
->each->toBeInstanceOf(CreateResponseCategory::class)
134+
->categoryAppliedInputTypes->toHaveCount(13);
135+
136+
expect($result->results[0]->categories[Category::ViolenceGraphic->value])
137+
->category->toBe(Category::ViolenceGraphic)
138+
->violated->toBe(true)
139+
->score->toBe(5.7929166992142E-5);
140+
141+
expect($result->results[0]->categoryAppliedInputTypes[Category::IllicitViolent->value])
142+
->toBe([CategoryAppliedInputType::Text->value]);
143+
144+
expect($result->results[0]->categoryAppliedInputTypes[Category::ViolenceGraphic->value])
145+
->toBe([CategoryAppliedInputType::Text->value, CategoryAppliedInputType::Image->value]);
146+
82147
});

tests/Testing/Resources/ModerationsTestResource.php

+31
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,34 @@
2020
$parameters['input'] === 'I want to k*** them.';
2121
});
2222
});
23+
24+
it('records a multi-modal moderations create request', function () {
25+
$fake = new ClientFake([
26+
CreateResponse::fake(),
27+
]);
28+
29+
$fake->moderations()->create([
30+
'model' => 'text-moderation-omni',
31+
'input' => [
32+
[
33+
'type' => 'text',
34+
'text' => 'I want to k*** them.',
35+
],
36+
[
37+
'type' => 'image_url',
38+
'image_url' => [
39+
'url' => 'https://example.com/potentially-harmful-image.jpg',
40+
],
41+
],
42+
],
43+
]);
44+
45+
$fake->assertSent(Moderations::class, function ($method, $parameters) {
46+
return $method === 'create' &&
47+
$parameters['model'] === 'text-moderation-omni' &&
48+
$parameters['input'][0]['type'] === 'text' &&
49+
$parameters['input'][0]['text'] === 'I want to k*** them.' &&
50+
$parameters['input'][1]['type'] === 'image_url' &&
51+
$parameters['input'][1]['image_url']['url'] === 'https://example.com/potentially-harmful-image.jpg';
52+
});
53+
});

0 commit comments

Comments
 (0)