Repository: softwaremill/sttp-openai (scala)

Issue:
createModeration throws  "Could not deserialize: text-moderation-007"
Not sure if it throws for everyone but it does for me:

```scala
//> using dep com.softwaremill.sttp.openai::core:0.0.10
import sttp.openai.OpenAISyncClient
import sttp.openai.requests.moderations.ModerationsRequestBody.ModerationsBody

val client = OpenAISyncClient("open-ai-authToken")
client.createModeration(ModerationsBody("some text"))
```

```
$ scala-cli ./moderation.sc 
Starting compilation server
Compiling project (Scala 3.4.0, JVM (21))
Compiled project (Scala 3.4.0, JVM (21))
Exception in thread "main" sttp.openai.OpenAIExceptions$OpenAIException$DeserializationOpenAIException: Could not deserialize: text-moderation-007
        at sttp.openai.OpenAIExceptions$OpenAIException$DeserializationOpenAIException$.apply(OpenAIExceptions.scala:22)
        at sttp.openai.OpenAIExceptions$OpenAIException$DeserializationOpenAIException$.apply(OpenAIExceptions.scala:24)
        at sttp.openai.json.SttpUpickleApiExtension$.deserializeJsonSnake$$anonfun$1(SttpUpickleApiExtension.scala:38)
        at sttp.openai.json.SttpUpickleApiExtension$.deserializeRightWithMappedExceptions$$anonfun$1(SttpUpickleApiExtension.scala:31)
        at sttp.client4.MappedResponseAs.mapWithMetadata$$anonfun$1(ResponseAs.scala:335)
        at sttp.client4.internal.BodyFromResponseAs.doApply$$anonfun$2$$anonfun$1$$anonfun$1(BodyFromResponseAs.scala:24)
        at sttp.client4.monad.IdMonad$.eval(IdMonad.scala:16)
        at sttp.client4.internal.BodyFromResponseAs.doApply$$anonfun$2$$anonfun$1(BodyFromResponseAs.scala:24)
        at sttp.monad.syntax$MonadErrorOps.map(MonadError.scala:60)
        at sttp.client4.internal.BodyFromResponseAs.doApply$$anonfun$2(BodyFromResponseAs.scala:24)
        at sttp.client4.monad.IdMonad$.flatMap(IdMonad.scala:9)
        at sttp.monad.syntax$MonadErrorOps.flatMap(MonadError.scala:61)
        at sttp.client4.internal.BodyFromResponseAs.doApply(BodyFromResponseAs.scala:25)
        at sttp.client4.internal.BodyFromResponseAs.apply$$anonfun$1(BodyFromResponseAs.scala:14)
        at sttp.monad.syntax$MonadErrorOps.map(MonadError.scala:60)
        at sttp.client4.internal.BodyFromResponseAs.apply(BodyFromResponseAs.scala:14)
        at sttp.client4.internal.httpclient.BodyFromHttpClient.apply(BodyFromHttpClient.scala:20)
        at sttp.client4.internal.httpclient.BodyFromHttpClient.apply$(BodyFromHttpClient.scala:11)
        at sttp.client4.httpclient.HttpClientSyncBackend$$anon$2.apply(HttpClientSyncBackend.scala:93)
        at sttp.client4.httpclient.HttpClientBackend.readResponse(HttpClientBackend.scala:126)
        at sttp.client4.httpclient.HttpClientSyncBackend.sendRegular(HttpClientSyncBackend.scala:41)
        at sttp.client4.httpclient.HttpClientSyncBackend.sendRegular(HttpClientSyncBackend.scala:38)
        at sttp.client4.httpclient.HttpClientBackend.send$$anonfun$1(HttpClientBackend.scala:46)
        at scala.util.Try$.apply(Try.scala:210)
        at sttp.monad.MonadError.handleError(MonadError.scala:24)
        at sttp.monad.MonadError.handleError$(MonadError.scala:16)
        at sttp.client4.monad.IdMonad$.handleError(IdMonad.scala:6)
        at sttp.client4.SttpClientException$.adjustExceptions(SttpClientException.scala:35)
        at sttp.client4.httpclient.HttpClientBackend.adjustExceptions(HttpClientBackend.scala:55)
        at sttp.client4.httpclient.HttpClientBackend.send(HttpClientBackend.scala:47)
        at sttp.client4.wrappers.FollowRedirectsBackend.sendWithCounter(FollowRedirectsBackend.scala:19)
        at sttp.client4.wrappers.FollowRedirectsBackend.send(FollowRedirectsBackend.scala:15)
        at sttp.client4.Request.send(request.scala:158)
        at sttp.openai.OpenAISyncClient.sendOrThrow(OpenAISyncClient.scala:791)
        at sttp.openai.OpenAISyncClient.createModeration(OpenAISyncClient.scala:331)
        at moderation$_.<init>(moderation.sc:10)
        at moderation_sc$.script$lzyINIT1(moderation.sc:25)
        at moderation_sc$.script(moderation.sc:25)
        at moderation_sc$.main(moderation.sc:29)
        at moderation_sc.main(moderation.sc)
Caused by: sttp.client4.DeserializationException: Could not deserialize: text-moderation-007
        at sttp.client4.DeserializationException$.apply(ResponseException.scala:10)
        ... 39 more
```

Pull request:
Fixes #162

Patch:
diff --git a/core/src/main/scala/sttp/openai/requests/moderations/ModerationsRequestBody.scala b/core/src/main/scala/sttp/openai/requests/moderations/ModerationsRequestBody.scala
index b0fc2cac..ddc980ca 100644
--- a/core/src/main/scala/sttp/openai/requests/moderations/ModerationsRequestBody.scala
+++ b/core/src/main/scala/sttp/openai/requests/moderations/ModerationsRequestBody.scala
@@ -24,7 +24,7 @@ object ModerationsRequestBody {
         jsonValue =>
           SnakePickle.read[ujson.Value](jsonValue) match {
             case Str(value) =>
-              byModerationModelValue.getOrElse(value, throw DeserializationOpenAIException(new Exception(s"Could not deserialize: $value")))
+              byModerationModelValue.getOrElse(value, CustomModerationModel(value))
             case e => throw DeserializationOpenAIException(new Exception(s"Could not deserialize: $e"))
           }
       )


Test patch:
diff --git a/core/src/test/scala/sttp/openai/fixtures/ModerationsFixture.scala b/core/src/test/scala/sttp/openai/fixtures/ModerationsFixture.scala
index 3d6d53b2..12512693 100644
--- a/core/src/test/scala/sttp/openai/fixtures/ModerationsFixture.scala
+++ b/core/src/test/scala/sttp/openai/fixtures/ModerationsFixture.scala
@@ -30,4 +30,33 @@ object ModerationsFixture {
                                        |  ]
                                        |}
                                        |""".stripMargin
+
+  val jsonCreateModerationResponseUnknownModel = """{
+                                       |  "id": "modr-5MWoL1",
+                                       |  "model": "text-moderation-007",
+                                       |  "results": [
+                                       |    {
+                                       |      "categories": {
+                                       |        "hate": false,
+                                       |        "hate/threatening": true,
+                                       |        "self-harm": false,
+                                       |        "sexual": false,
+                                       |        "sexual/minors": false,
+                                       |        "violence": true,
+                                       |        "violence/graphic": false
+                                       |      },
+                                       |      "category_scores": {
+                                       |        "hate": 0.22714105248451233,
+                                       |        "hate/threatening": 0.4132447838783264,
+                                       |        "self-harm": 0.005232391878962517,
+                                       |        "sexual": 0.01407341007143259,
+                                       |        "sexual/minors": 0.0038522258400917053,
+                                       |        "violence": 0.9223177433013916,
+                                       |        "violence/graphic": 0.036865197122097015
+                                       |      },
+                                       |      "flagged": true
+                                       |    }
+                                       |  ]
+                                       |}
+                                       |""".stripMargin
 }
diff --git a/core/src/test/scala/sttp/openai/requests/moderations/ModerationsDataSpec.scala b/core/src/test/scala/sttp/openai/requests/moderations/ModerationsDataSpec.scala
index 99861a39..6b0b90cd 100644
--- a/core/src/test/scala/sttp/openai/requests/moderations/ModerationsDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/moderations/ModerationsDataSpec.scala
@@ -11,7 +11,7 @@ import sttp.openai.requests.moderations.ModerationsResponseData._
 class ModerationsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
   "Given create moderation response as Json" should "be properly deserialized to case class" in {
     // given
-    val listFilesResponse = fixtures.ModerationsFixture.jsonCreateModerationResponse
+    val createModerationResponse = fixtures.ModerationsFixture.jsonCreateModerationResponse
     val expectedResponse = ModerationData(
       id = "modr-5MWoLO",
       model = ModerationModel.TextModerationStable,
@@ -41,10 +41,20 @@ class ModerationsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
     // when
     val givenResponse: Either[Exception, ModerationData] =
-      SttpUpickleApiExtension.deserializeJsonSnake[ModerationData].apply(listFilesResponse)
+      SttpUpickleApiExtension.deserializeJsonSnake[ModerationData].apply(createModerationResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
   }
+  "Unknown moderation model in create response" should "be deserialized to case class" in {
+    // given
+    val createModerationResponse = fixtures.ModerationsFixture.jsonCreateModerationResponseUnknownModel
+    // when
+    val givenResponse: Either[Exception, ModerationData] =
+      SttpUpickleApiExtension.deserializeJsonSnake[ModerationData].apply(createModerationResponse)
+
+    // then
+    givenResponse.value.model shouldBe ModerationModel.CustomModerationModel("text-moderation-007")      
+  }
 
 }