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..9687b040 100644 --- a/src/Responses/Moderations/CreateResponseResult.php +++ b/src/Responses/Moderations/CreateResponseResult.php @@ -10,16 +10,18 @@ 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 +42,13 @@ 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 +59,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..b1ca93fa 100644 --- a/tests/Fixtures/Moderation.php +++ b/tests/Fixtures/Moderation.php @@ -83,6 +83,82 @@ 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'], + ], + ], + ], + ]; +} + +/** + * @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 7e5dff91..e531f981 100644 --- a/tests/Resources/Moderations.php +++ b/tests/Resources/Moderations.php @@ -1,12 +1,20 @@ [ + ['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', @@ -44,15 +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' => 'I want to kill them.', + 'input' => $input, ], Response::from(moderationOmniResource(), metaHeaders())); $result = $client->moderations()->create([ 'model' => 'omni-moderation-latest', - 'input' => 'I want to kill them.', + 'input' => $input, ]); expect($result) @@ -77,6 +85,63 @@ ->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); +})->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]); + }); 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'; + }); +});