From 5e4d05770d58cf3bc4d12c62e6d0a220a822538f Mon Sep 17 00:00:00 2001 From: Makenson Petit-Fils Clervil Date: Mon, 28 Apr 2025 10:32:29 -0600 Subject: [PATCH 1/7] Add support for multi-modal moderation inputs and category applied input types --- .../Moderations/CategoryAppliedInputType.php | 12 +++++++ src/Resources/Moderations.php | 13 +++++++- src/Responses/Moderations/CreateResponse.php | 6 ++-- .../Moderations/CreateResponseResult.php | 27 +++++++++++++--- tests/Fixtures/Moderation.php | 15 +++++++++ tests/Resources/Moderations.php | 13 ++++++-- .../Resources/ModerationsTestResource.php | 31 +++++++++++++++++++ 7 files changed, 107 insertions(+), 10 deletions(-) create mode 100644 src/Enums/Moderations/CategoryAppliedInputType.php diff --git a/src/Enums/Moderations/CategoryAppliedInputType.php b/src/Enums/Moderations/CategoryAppliedInputType.php new file mode 100644 index 00000000..e300abae --- /dev/null +++ b/src/Enums/Moderations/CategoryAppliedInputType.php @@ -0,0 +1,12 @@ +, category_scores: array, flagged: bool}>}> $response */ + /** + * @var Response, + * category_scores: array, + * flagged: bool, + * category_applied_input_types?: array> + * }> + * }> $response + */ $response = $this->transporter->requestObject($payload); return CreateResponse::from($response->data(), $response->meta()); diff --git a/src/Responses/Moderations/CreateResponse.php b/src/Responses/Moderations/CreateResponse.php index 7815ca50..369f1b0d 100644 --- a/src/Responses/Moderations/CreateResponse.php +++ b/src/Responses/Moderations/CreateResponse.php @@ -12,12 +12,12 @@ use OpenAI\Testing\Responses\Concerns\Fakeable; /** - * @implements ResponseContract, category_scores: array, flagged: bool}>}> + * @implements ResponseContract, category_scores: array, flagged: bool, category_applied_input_types?: array>}>}> */ final class CreateResponse implements ResponseContract, ResponseHasMetaInformationContract { /** - * @use ArrayAccessible, category_scores: array, flagged: bool}>}> + * @use ArrayAccessible, category_scores: array, flagged: bool, category_applied_input_types?: array>}>}> */ use ArrayAccessible; @@ -37,7 +37,7 @@ private function __construct( /** * Acts as static factory, and returns a new Response instance. * - * @param array{id: string, model: string, results: array, category_scores: array, flagged: bool}>} $attributes + * @param array{id: string, model: string, results: array, category_scores: array, flagged: bool, category_applied_input_types?: array>}>} $attributes */ public static function from(array $attributes, MetaInformation $meta): self { diff --git a/src/Responses/Moderations/CreateResponseResult.php b/src/Responses/Moderations/CreateResponseResult.php index a913dd75..748ea501 100644 --- a/src/Responses/Moderations/CreateResponseResult.php +++ b/src/Responses/Moderations/CreateResponseResult.php @@ -10,16 +10,23 @@ final class CreateResponseResult { /** * @param array $categories + * @param array> $categoryAppliedInputTypes */ private function __construct( public readonly array $categories, public readonly bool $flagged, + public readonly ?array $categoryAppliedInputTypes, ) { // .. } /** - * @param array{categories: array, category_scores: array, flagged: bool} $attributes + * @param array{ + * categories: array, + * category_scores: array, + * flagged: bool, + * category_applied_input_types?: array> + * } $attributes */ public static function from(array $attributes): self { @@ -40,12 +47,18 @@ public static function from(array $attributes): self return new CreateResponseResult( $categories, - $attributes['flagged'] + $attributes['flagged'], + $attributes['category_applied_input_types'] ?? null, ); } /** - * @return array{categories: array, category_scores: array, flagged: bool} + * @return array{ + * categories: array, + * category_scores: array, + * flagged: bool, + * category_applied_input_types?: array> + * } */ public function toArray(): array { @@ -56,10 +69,16 @@ public function toArray(): array $categoryScores[$category->category->value] = $category->score; } - return [ + $result = [ 'categories' => $categories, 'category_scores' => $categoryScores, 'flagged' => $this->flagged, ]; + + if ($this->categoryAppliedInputTypes !== null) { + $result['category_applied_input_types'] = $this->categoryAppliedInputTypes; + } + + return $result; } } diff --git a/tests/Fixtures/Moderation.php b/tests/Fixtures/Moderation.php index 9b7c2ab2..a6862c79 100644 --- a/tests/Fixtures/Moderation.php +++ b/tests/Fixtures/Moderation.php @@ -83,6 +83,21 @@ function moderationOmniResource(): array 'violence/graphic' => 0.036865197122097015, ], 'flagged' => true, + 'category_applied_input_types' => [ + 'hate' => ['text'], + 'hate/threatening' => ['text'], + 'harassment' => ['text'], + 'harassment/threatening' => ['text'], + 'self-harm' => ['text'], + 'self-harm/intent' => ['text'], + 'self-harm/instructions' => ['text'], + 'sexual' => ['text'], + 'sexual/minors' => ['text'], + 'violence' => ['text'], + 'violence/graphic' => ['text'], + 'illicit' => ['text'], + 'illicit/violent' => ['text'], + ], ], ], ]; diff --git a/tests/Resources/Moderations.php b/tests/Resources/Moderations.php index 7e5dff91..2ed1929b 100644 --- a/tests/Resources/Moderations.php +++ b/tests/Resources/Moderations.php @@ -1,6 +1,7 @@ 'omni-moderation-latest', - 'input' => 'I want to kill them.', + 'input' => [ + ['type' => 'text', 'text' => '.. I want to kill...'], + ], ], Response::from(moderationOmniResource(), metaHeaders())); $result = $client->moderations()->create([ 'model' => 'omni-moderation-latest', - 'input' => 'I want to kill them.', + 'input' => [ + ['type' => 'text', 'text' => '.. I want to kill...'], + ], ]); expect($result) @@ -77,6 +82,10 @@ ->violated->toBe(true) ->score->toBe(0.9223177433013916); + expect($result->results[0]->categoryAppliedInputTypes) + ->toHaveCount(13) + ->each->toBe([CategoryAppliedInputType::Text->value]); + expect($result->meta()) ->toBeInstanceOf(MetaInformation::class); }); diff --git a/tests/Testing/Resources/ModerationsTestResource.php b/tests/Testing/Resources/ModerationsTestResource.php index d2c0709c..a86a64a8 100644 --- a/tests/Testing/Resources/ModerationsTestResource.php +++ b/tests/Testing/Resources/ModerationsTestResource.php @@ -20,3 +20,34 @@ $parameters['input'] === 'I want to k*** them.'; }); }); + +it('records a multi-modal moderations create request', function () { + $fake = new ClientFake([ + CreateResponse::fake(), + ]); + + $fake->moderations()->create([ + 'model' => 'text-moderation-omni', + 'input' => [ + [ + 'type' => 'text', + 'text' => 'I want to k*** them.', + ], + [ + 'type' => 'image_url', + 'image_url' => [ + 'url' => 'https://example.com/potentially-harmful-image.jpg', + ], + ], + ], + ]); + + $fake->assertSent(Moderations::class, function ($method, $parameters) { + return $method === 'create' && + $parameters['model'] === 'text-moderation-omni' && + $parameters['input'][0]['type'] === 'text' && + $parameters['input'][0]['text'] === 'I want to k*** them.' && + $parameters['input'][1]['type'] === 'image_url' && + $parameters['input'][1]['image_url']['url'] === 'https://example.com/potentially-harmful-image.jpg'; + }); +}); From f7dfff538f98a30faf59772f87687fead23d475b Mon Sep 17 00:00:00 2001 From: Makenson Petit-Fils Clervil Date: Mon, 28 Apr 2025 11:05:18 -0600 Subject: [PATCH 2/7] put parameter documentation in single line --- src/Responses/Moderations/CreateResponseResult.php | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Responses/Moderations/CreateResponseResult.php b/src/Responses/Moderations/CreateResponseResult.php index 748ea501..cdc66d1d 100644 --- a/src/Responses/Moderations/CreateResponseResult.php +++ b/src/Responses/Moderations/CreateResponseResult.php @@ -20,14 +20,7 @@ private function __construct( // .. } - /** - * @param array{ - * categories: array, - * category_scores: array, - * flagged: bool, - * category_applied_input_types?: array> - * } $attributes - */ + /** @param array{categories: array, category_scores: array, flagged: bool, category_applied_input_types?: array>} $attributes */ public static function from(array $attributes): self { /** @var array $categories */ From 8f72ca929fd9fae8aea6d1dfc3e3e46479e16cbb Mon Sep 17 00:00:00 2001 From: Makenson Petit-Fils Clervil Date: Mon, 28 Apr 2025 12:31:10 -0600 Subject: [PATCH 3/7] Add support for omni moderation with text and image inputs --- tests/Fixtures/Moderation.php | 61 ++++++++++++++++++++++++++++ tests/Resources/Moderations.php | 70 +++++++++++++++++++++++++++++---- 2 files changed, 124 insertions(+), 7 deletions(-) diff --git a/tests/Fixtures/Moderation.php b/tests/Fixtures/Moderation.php index a6862c79..b1ca93fa 100644 --- a/tests/Fixtures/Moderation.php +++ b/tests/Fixtures/Moderation.php @@ -102,3 +102,64 @@ function moderationOmniResource(): array ], ]; } + +/** + * @return array + */ +function moderationOmniWithTextAndImageResource(): array +{ + return [ + 'id' => 'modr-5MWoLO', + 'model' => 'omni-moderation-001', + 'results' => [ + [ + 'categories' => [ + 'hate' => false, + 'hate/threatening' => false, + 'harassment' => false, + 'harassment/threatening' => false, + 'illicit' => false, + 'illicit/violent' => false, + 'self-harm' => false, + 'self-harm/intent' => false, + 'self-harm/instructions' => false, + 'sexual' => false, + 'sexual/minors' => false, + 'violence' => true, + 'violence/graphic' => true, + ], + 'category_scores' => [ + 'hate' => 0.22714105248451233, + 'hate/threatening' => 0.4132447838783264, + 'illicit' => 0.1602763684674149, + 'illicit/violent' => 0.9223177433013916, + 'harassment' => 0.1602763684674149, + 'harassment/threatening' => 0.1602763684674149, + 'self-harm' => 0.005232391878962517, + 'self-harm/intent' => 0.005134391873962517, + 'self-harm/instructions' => 0.005132591874962517, + 'sexual' => 0.01407341007143259, + 'sexual/minors' => 0.0038522258400917053, + 'violence' => 0.4132447838783264, + 'violence/graphic' => 5.7929166992142E-5, + ], + 'flagged' => true, + 'category_applied_input_types' => [ + 'hate' => ['text'], + 'hate/threatening' => ['text'], + 'harassment' => ['text'], + 'harassment/threatening' => ['text'], + 'self-harm' => ['text', 'image'], + 'self-harm/intent' => ['text', 'image'], + 'self-harm/instructions' => ['text', 'image'], + 'sexual' => ['text', 'image'], + 'sexual/minors' => ['text', 'image'], + 'violence' => ['text', 'image'], + 'violence/graphic' => ['text', 'image'], + 'illicit' => ['text'], + 'illicit/violent' => ['text'], + ], + ], + ], + ]; +} diff --git a/tests/Resources/Moderations.php b/tests/Resources/Moderations.php index 2ed1929b..e531f981 100644 --- a/tests/Resources/Moderations.php +++ b/tests/Resources/Moderations.php @@ -8,6 +8,13 @@ use OpenAI\Responses\Moderations\CreateResponseResult; use OpenAI\ValueObjects\Transporter\Response; +dataset('create omni inputs', [ + 'text_in_array' => [ + ['type' => 'text', 'text' => 'I love to kill...'], + ], + 'basic_text' => 'I want to kill them.', +]); + test('create legacy', closure: function () { $client = mockClient('POST', 'moderations', [ 'model' => 'text-moderation-latest', @@ -45,19 +52,15 @@ ->toBeInstanceOf(MetaInformation::class); }); -test('create omni', closure: function () { +test('create omni', closure: function ($input) { $client = mockClient('POST', 'moderations', [ 'model' => 'omni-moderation-latest', - 'input' => [ - ['type' => 'text', 'text' => '.. I want to kill...'], - ], + 'input' => $input, ], Response::from(moderationOmniResource(), metaHeaders())); $result = $client->moderations()->create([ 'model' => 'omni-moderation-latest', - 'input' => [ - ['type' => 'text', 'text' => '.. I want to kill...'], - ], + 'input' => $input, ]); expect($result) @@ -88,4 +91,57 @@ expect($result->meta()) ->toBeInstanceOf(MetaInformation::class); +})->with('create omni inputs'); + +test('create omni with image and text', closure: function () { + $client = mockClient('POST', 'moderations', [ + 'model' => 'omni-moderation-latest', + 'input' => [ + ['type' => 'text', 'text' => '.. I want to kill...'], + [ + 'type' => 'image_url', + 'image_url' => [ + 'url' => 'https://example.com/image.png', + ], + ], + ], + ], Response::from(moderationOmniWithTextAndImageResource(), metaHeaders())); + + $result = $client->moderations()->create([ + 'model' => 'omni-moderation-latest', + 'input' => [ + ['type' => 'text', 'text' => '.. I want to kill...'], + [ + 'type' => 'image_url', + 'image_url' => [ + 'url' => 'https://example.com/image.png', + ], + ], + ], + ]); + + expect($result) + ->toBeInstanceOf(CreateResponse::class) + ->id->toBe('modr-5MWoLO') + ->model->toBe('omni-moderation-001') + ->results->toBeArray()->toHaveCount(1) + ->results->each->toBeInstanceOf(CreateResponseResult::class); + + expect($result->results[0]) + ->flagged->toBeTrue() + ->categories->toHaveCount(13) + ->each->toBeInstanceOf(CreateResponseCategory::class) + ->categoryAppliedInputTypes->toHaveCount(13); + + expect($result->results[0]->categories[Category::ViolenceGraphic->value]) + ->category->toBe(Category::ViolenceGraphic) + ->violated->toBe(true) + ->score->toBe(5.7929166992142E-5); + + expect($result->results[0]->categoryAppliedInputTypes[Category::IllicitViolent->value]) + ->toBe([CategoryAppliedInputType::Text->value]); + + expect($result->results[0]->categoryAppliedInputTypes[Category::ViolenceGraphic->value]) + ->toBe([CategoryAppliedInputType::Text->value, CategoryAppliedInputType::Image->value]); + }); From da9bd98c3de55c918b6bed9e7d829e0a1a85055c Mon Sep 17 00:00:00 2001 From: Makenson Petit-Fils Clervil Date: Mon, 28 Apr 2025 13:34:24 -0600 Subject: [PATCH 4/7] put DocBlock in single line --- src/Resources/Moderations.php | 13 +------------ src/Responses/Moderations/CreateResponseResult.php | 9 +-------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/src/Resources/Moderations.php b/src/Resources/Moderations.php index 3a468310..2f296974 100644 --- a/src/Resources/Moderations.php +++ b/src/Resources/Moderations.php @@ -24,18 +24,7 @@ public function create(array $parameters): CreateResponse { $payload = Payload::create('moderations', $parameters); - /** - * @var Response, - * category_scores: array, - * flagged: bool, - * category_applied_input_types?: array> - * }> - * }> $response - */ + /** @var Response, category_scores: array,flagged: bool,category_applied_input_types?: array>}>}> $response */ $response = $this->transporter->requestObject($payload); return CreateResponse::from($response->data(), $response->meta()); diff --git a/src/Responses/Moderations/CreateResponseResult.php b/src/Responses/Moderations/CreateResponseResult.php index cdc66d1d..dc2ff7e0 100644 --- a/src/Responses/Moderations/CreateResponseResult.php +++ b/src/Responses/Moderations/CreateResponseResult.php @@ -45,14 +45,7 @@ public static function from(array $attributes): self ); } - /** - * @return array{ - * categories: array, - * category_scores: array, - * flagged: bool, - * category_applied_input_types?: array> - * } - */ + /** @return array{ categories: array, category_scores: array, flagged: bool, category_applied_input_types?: array>} */ public function toArray(): array { $categories = []; From 2a6c324f0a55cb5c6eb71a63a203a6d91acd9776 Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Wed, 30 Apr 2025 10:20:57 -0400 Subject: [PATCH 5/7] chore: remove unneeded docblock changes --- src/Resources/Moderations.php | 2 +- src/Responses/Moderations/CreateResponseResult.php | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Resources/Moderations.php b/src/Resources/Moderations.php index 2f296974..da2134d5 100644 --- a/src/Resources/Moderations.php +++ b/src/Resources/Moderations.php @@ -24,7 +24,7 @@ public function create(array $parameters): CreateResponse { $payload = Payload::create('moderations', $parameters); - /** @var Response, category_scores: array,flagged: bool,category_applied_input_types?: array>}>}> $response */ + /** @var Response, category_scores: array,flagged: bool,category_applied_input_types?: array>}>}> $response */ $response = $this->transporter->requestObject($payload); return CreateResponse::from($response->data(), $response->meta()); diff --git a/src/Responses/Moderations/CreateResponseResult.php b/src/Responses/Moderations/CreateResponseResult.php index dc2ff7e0..c375ae90 100644 --- a/src/Responses/Moderations/CreateResponseResult.php +++ b/src/Responses/Moderations/CreateResponseResult.php @@ -20,7 +20,9 @@ private function __construct( // .. } - /** @param array{categories: array, category_scores: array, flagged: bool, category_applied_input_types?: array>} $attributes */ + /** + * @param array{categories: array, category_scores: array, flagged: bool, category_applied_input_types?: array>} $attributes + */ public static function from(array $attributes): self { /** @var array $categories */ @@ -45,7 +47,9 @@ public static function from(array $attributes): self ); } - /** @return array{ categories: array, category_scores: array, flagged: bool, category_applied_input_types?: array>} */ + /** + * @return array{ categories: array, category_scores: array, flagged: bool, category_applied_input_types?: array>} + */ public function toArray(): array { $categories = []; From c516fed5d0fbb8d64ac1f8d0cb008b0fe24f5c8a Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Wed, 30 Apr 2025 10:23:15 -0400 Subject: [PATCH 6/7] chore: unneeded spacing change --- src/Resources/Moderations.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Resources/Moderations.php b/src/Resources/Moderations.php index da2134d5..77c23044 100644 --- a/src/Resources/Moderations.php +++ b/src/Resources/Moderations.php @@ -24,7 +24,7 @@ public function create(array $parameters): CreateResponse { $payload = Payload::create('moderations', $parameters); - /** @var Response, category_scores: array,flagged: bool,category_applied_input_types?: array>}>}> $response */ + /** @var Response, category_scores: array, flagged: bool,category_applied_input_types?: array>}>}> $response */ $response = $this->transporter->requestObject($payload); return CreateResponse::from($response->data(), $response->meta()); From 31d844918f7b583cdcb247a93f744071a3708ddb Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Wed, 30 Apr 2025 10:27:03 -0400 Subject: [PATCH 7/7] chore: pint --- src/Responses/Moderations/CreateResponseResult.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Responses/Moderations/CreateResponseResult.php b/src/Responses/Moderations/CreateResponseResult.php index c375ae90..9687b040 100644 --- a/src/Responses/Moderations/CreateResponseResult.php +++ b/src/Responses/Moderations/CreateResponseResult.php @@ -21,7 +21,7 @@ private function __construct( } /** - * @param array{categories: array, category_scores: array, flagged: bool, category_applied_input_types?: array>} $attributes + * @param array{categories: array, category_scores: array, flagged: bool, category_applied_input_types?: array>} $attributes */ public static function from(array $attributes): self {