Repository: softwaremill/sttp-openai
Pull request #78 (2023-05-08 10:45:00)

Moderations model adt


Patch:
diff --git a/core/src/main/scala/sttp/openai/OpenAI.scala b/core/src/main/scala/sttp/openai/OpenAI.scala
index 42d8010..082885e 100644
--- a/core/src/main/scala/sttp/openai/OpenAI.scala
+++ b/core/src/main/scala/sttp/openai/OpenAI.scala
@@ -12,7 +12,7 @@ import sttp.openai.requests.completions.edit.EditRequestResponseData.EditRespons
 import sttp.openai.requests.embeddings.EmbeddingsRequestBody.EmbeddingsBody
 import sttp.openai.requests.embeddings.EmbeddingsResponseBody.EmbeddingResponse
 import sttp.openai.requests.files.FilesResponseData._
-import sttp.openai.requests.finetunes.FineTunesRequestBody
+import sttp.openai.requests.finetunes.{FineTuneModel, FineTunesRequestBody}
 import sttp.openai.requests.finetunes.FineTunesResponseData.{
   DeleteFineTuneModelResponse,
   FineTuneEventsResponse,
@@ -561,9 +561,9 @@ class OpenAI(authToken: String) {
     * @param model
     *   The model to delete.
     */
-  def deleteFineTuneModel(model: String): Request[Either[ResponseException[String, Exception], DeleteFineTuneModelResponse]] =
+  def deleteFineTuneModel(model: FineTuneModel): Request[Either[ResponseException[String, Exception], DeleteFineTuneModelResponse]] =
     openApiAuthRequest
-      .delete(OpenAIUris.fineTuneModel(model))
+      .delete(OpenAIUris.fineTuneModel(model.value))
       .response(asJsonSnake[DeleteFineTuneModelResponse])
 
   /** Get fine-grained status updates for a fine-tune job.
diff --git a/core/src/main/scala/sttp/openai/requests/completions/edit/EditRequestBody.scala b/core/src/main/scala/sttp/openai/requests/completions/edit/EditRequestBody.scala
index 88018e1..6789c86 100644
--- a/core/src/main/scala/sttp/openai/requests/completions/edit/EditRequestBody.scala
+++ b/core/src/main/scala/sttp/openai/requests/completions/edit/EditRequestBody.scala
@@ -1,11 +1,12 @@
 package sttp.openai.requests.completions.edit
 
-import sttp.openai.json.SnakePickle
+import sttp.openai.json.{DeserializationException, SnakePickle}
+import ujson.Str
 
 object EditRequestBody {
 
   /** @param model
-    *   ID of the model to use. You can use the `text-davinci-edit-001` or `code-davinci-edit-001` model with this endpoint.
+    *   ID of the [[EditModel]] to use.
     * @param input
     *   The input text to use as a starting point for the edit.
     * @param instruction
@@ -20,7 +21,7 @@ object EditRequestBody {
     *   probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.
     */
   case class EditBody(
-      model: String,
+      model: EditModel,
       input: Option[String] = None,
       instruction: String,
       n: Option[Int] = None,
@@ -31,4 +32,29 @@ object EditRequestBody {
   object EditBody {
     implicit val editBodyW: SnakePickle.Writer[EditBody] = SnakePickle.macroW[EditBody]
   }
+
+  sealed abstract class EditModel(val value: String)
+
+  object EditModel {
+    implicit val editModelRW: SnakePickle.ReadWriter[EditModel] = SnakePickle
+      .readwriter[ujson.Value]
+      .bimap[EditModel](
+        model => SnakePickle.writeJs(model.value),
+        jsonValue =>
+          SnakePickle.read[ujson.Value](jsonValue) match {
+            case Str(value) =>
+              byEditModelValue.getOrElse(value, throw new DeserializationException(new Exception(s"Could not deserialize: $value")))
+            case e => throw new DeserializationException(new Exception(s"Could not deserialize: $e"))
+          }
+      )
+
+    case object TextDavinciEdit001 extends EditModel("text-davinci-edit-001")
+    case object CodeDavinciEdit001 extends EditModel("code-davinci-edit-001")
+    case class CustomEditModel(customEditModel: String) extends EditModel(customEditModel)
+
+    val values: Set[EditModel] = Set(TextDavinciEdit001, CodeDavinciEdit001)
+
+    private val byEditModelValue = values.map(model => model.value -> model).toMap
+  }
+
 }
diff --git a/core/src/main/scala/sttp/openai/requests/finetunes/FineTunesRequestBody.scala b/core/src/main/scala/sttp/openai/requests/finetunes/FineTunesRequestBody.scala
index 6ab8449..1087b52 100644
--- a/core/src/main/scala/sttp/openai/requests/finetunes/FineTunesRequestBody.scala
+++ b/core/src/main/scala/sttp/openai/requests/finetunes/FineTunesRequestBody.scala
@@ -1,6 +1,7 @@
 package sttp.openai.requests.finetunes
 
-import sttp.openai.json.SnakePickle
+import sttp.openai.json.{DeserializationException, SnakePickle}
+import ujson.Str
 
 /** @param trainingFile
   *   The ID of an uploaded file that contains training data.
@@ -37,7 +38,7 @@ import sttp.openai.json.SnakePickle
 case class FineTunesRequestBody(
     trainingFile: String,
     validationFile: Option[String] = None,
-    model: Option[String] = None,
+    model: Option[FineTuneModel] = None,
     nEpochs: Option[Int] = None,
     batchSize: Option[Int] = None,
     learningRateMultiplier: Option[Double] = None,
@@ -51,3 +52,31 @@ case class FineTunesRequestBody(
 object FineTunesRequestBody {
   implicit val fineTunesRequestBodyWriter: SnakePickle.Writer[FineTunesRequestBody] = SnakePickle.macroW[FineTunesRequestBody]
 }
+
+sealed abstract class FineTuneModel(val value: String)
+
+object FineTuneModel {
+
+  implicit val fineTuneModelRW: SnakePickle.ReadWriter[FineTuneModel] = SnakePickle
+    .readwriter[ujson.Value]
+    .bimap[FineTuneModel](
+      model => SnakePickle.writeJs(model.value),
+      jsonValue =>
+        SnakePickle.read[ujson.Value](jsonValue) match {
+          case Str(value) =>
+            byFineTuneModelValue.getOrElse(value, throw new DeserializationException(new Exception(s"Could not deserialize: $value")))
+          case e => throw new DeserializationException(new Exception(s"Could not deserialize: $e"))
+        }
+    )
+
+  case object Davinci extends FineTuneModel("davinci")
+  case object Curie extends FineTuneModel("curie")
+  case object Babbage extends FineTuneModel("babbage")
+  case object Ada extends FineTuneModel("ada")
+
+  case class CustomFineTuneModel(customFineTuneModel: String) extends FineTuneModel(customFineTuneModel)
+
+  val values: Set[FineTuneModel] = Set(Davinci, Curie, Babbage, Ada)
+
+  private val byFineTuneModelValue = values.map(model => model.value -> model).toMap
+}
diff --git a/core/src/main/scala/sttp/openai/requests/finetunes/FineTunesResponseData.scala b/core/src/main/scala/sttp/openai/requests/finetunes/FineTunesResponseData.scala
index 5ca0edc..3d5ca1b 100644
--- a/core/src/main/scala/sttp/openai/requests/finetunes/FineTunesResponseData.scala
+++ b/core/src/main/scala/sttp/openai/requests/finetunes/FineTunesResponseData.scala
@@ -47,7 +47,7 @@ object FineTunesResponseData {
       id: String,
       hyperparams: Hyperparams,
       organizationId: String,
-      model: String,
+      model: FineTuneModel,
       trainingFiles: Seq[FileData],
       validationFiles: Seq[FileData],
       resultFiles: Seq[FileData],
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 f630fb1..13e71e0 100644
--- a/core/src/main/scala/sttp/openai/requests/moderations/ModerationsRequestBody.scala
+++ b/core/src/main/scala/sttp/openai/requests/moderations/ModerationsRequestBody.scala
@@ -1,32 +1,41 @@
 package sttp.openai.requests.moderations
 
-import sttp.openai.json.SnakePickle
+import sttp.openai.json.{DeserializationException, SnakePickle}
+import ujson.Str
 object ModerationsRequestBody {
 
   /** @param input
     *   The input text to classify.
     * @param model
-    *   Specifies content moderation models.
+    *   Specifies content moderation models of [[ModerationModel]].
     */
   case class ModerationsBody(input: String, model: Option[ModerationModel] = None)
 
   object ModerationsBody {
     implicit val moderationsBodyWriter: SnakePickle.Writer[ModerationsBody] = SnakePickle.macroW[ModerationsBody]
   }
-  sealed trait ModerationModel
+  sealed abstract class ModerationModel(val value: String)
   object ModerationModel {
-    case object TextModerationStable extends ModerationModel
+    implicit val moderationsBodyWriter: SnakePickle.ReadWriter[ModerationModel] = SnakePickle
+      .readwriter[ujson.Value]
+      .bimap[ModerationModel](
+        model => SnakePickle.writeJs(model.value),
+        jsonValue =>
+          SnakePickle.read[ujson.Value](jsonValue) match {
+            case Str(value) =>
+              byModerationModelValue.getOrElse(value, throw new DeserializationException(new Exception(s"Could not deserialize: $value")))
+            case e => throw new DeserializationException(new Exception(s"Could not deserialize: $e"))
+          }
+      )
 
-    case object TextModerationLatest extends ModerationModel
+    case object TextModerationStable extends ModerationModel("text-moderation-stable")
 
-    case class Custom(value: String) extends ModerationModel
+    case object TextModerationLatest extends ModerationModel("text-moderation-latest")
 
-    implicit val moderationsBodyWriter: SnakePickle.Writer[ModerationModel] =
-      SnakePickle.writer[String].comap[ModerationModel] {
-        case TextModerationStable => "text-moderation-stable"
-        case TextModerationLatest => "text-moderation-latest"
-        case Custom(value)        => value
-      }
+    case class CustomModerationModel(customModerationModel: String) extends ModerationModel(customModerationModel)
 
+    val values: Set[ModerationModel] = Set(TextModerationStable, TextModerationLatest)
+
+    private val byModerationModelValue = values.map(model => model.value -> model).toMap
   }
 }
diff --git a/core/src/main/scala/sttp/openai/requests/moderations/ModerationsResponseData.scala b/core/src/main/scala/sttp/openai/requests/moderations/ModerationsResponseData.scala
index 20b2c05..0cfd42a 100644
--- a/core/src/main/scala/sttp/openai/requests/moderations/ModerationsResponseData.scala
+++ b/core/src/main/scala/sttp/openai/requests/moderations/ModerationsResponseData.scala
@@ -1,6 +1,7 @@
 package sttp.openai.requests.moderations
 
 import sttp.openai.json.SnakePickle
+import sttp.openai.requests.moderations.ModerationsRequestBody.ModerationModel
 
 object ModerationsResponseData {
   case class CategoryScores(
@@ -40,7 +41,7 @@ object ModerationsResponseData {
   }
   case class ModerationData(
       id: String,
-      model: String,
+      model: ModerationModel,
       results: Seq[Result]
   )
   object ModerationData {


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 3df8093..3d6d53b 100644
--- a/core/src/test/scala/sttp/openai/fixtures/ModerationsFixture.scala
+++ b/core/src/test/scala/sttp/openai/fixtures/ModerationsFixture.scala
@@ -4,7 +4,7 @@ object ModerationsFixture {
 
   val jsonCreateModerationResponse = """{
                                        |  "id": "modr-5MWoLO",
-                                       |  "model": "text-moderation-001",
+                                       |  "model": "text-moderation-stable",
                                        |  "results": [
                                        |    {
                                        |      "categories": {
diff --git a/core/src/test/scala/sttp/openai/requests/completions/EditDataSpec.scala b/core/src/test/scala/sttp/openai/requests/completions/EditDataSpec.scala
index 165e117..6827f78 100644
--- a/core/src/test/scala/sttp/openai/requests/completions/EditDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/completions/EditDataSpec.scala
@@ -6,6 +6,7 @@ import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 import sttp.openai.json.{SnakePickle, SttpUpickleApiExtension}
 import sttp.openai.requests.completions.edit.EditRequestBody._
+import sttp.openai.requests.completions.edit.EditRequestBody.EditModel.TextDavinciEdit001
 
 class EditDataSpec extends AnyFlatSpec with Matchers with EitherValues {
 
@@ -44,7 +45,7 @@ class EditDataSpec extends AnyFlatSpec with Matchers with EitherValues {
   "Given completions request as case class" should "be properly serialized to Json" in {
     // given
     val givenRequest: EditBody = EditBody(
-      model = "text-davinci-edit-001",
+      model = TextDavinciEdit001,
       input = Some("What day of the wek is it?"),
       instruction = "Fix the spelling mistakes"
     )
diff --git a/core/src/test/scala/sttp/openai/requests/finetunes/FineTunesDataSpec.scala b/core/src/test/scala/sttp/openai/requests/finetunes/FineTunesDataSpec.scala
index 1d8a071..888a99a 100644
--- a/core/src/test/scala/sttp/openai/requests/finetunes/FineTunesDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/finetunes/FineTunesDataSpec.scala
@@ -24,7 +24,7 @@ class FineTunesDataSpec extends AnyFlatSpec with Matchers with EitherValues {
           learningRateMultiplier = None
         ),
         organizationId = "org-org123",
-        model = "curie",
+        model = FineTuneModel.Curie,
         trainingFiles = Seq(
           FileData(
             `object` = "file",
@@ -67,7 +67,7 @@ class FineTunesDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     val givenRequest = FineTunesRequestBody(
       trainingFile = "file-train231",
       validationFile = Some("file-valid2"),
-      model = Some("curie"),
+      model = Some(FineTuneModel.Curie),
       nEpochs = Some(3),
       batchSize = Some(2),
       learningRateMultiplier = Some(0.04d),
@@ -104,7 +104,7 @@ class FineTunesDataSpec extends AnyFlatSpec with Matchers with EitherValues {
             learningRateMultiplier = Some(0.1)
           ),
           organizationId = "org-9Pr8JxSaUX4Czeu1It3IT3hz",
-          model = "curie",
+          model = FineTuneModel.Curie,
           trainingFiles = Seq(
             FileData(
               `object` = "file",
@@ -161,7 +161,7 @@ class FineTunesDataSpec extends AnyFlatSpec with Matchers with EitherValues {
           learningRateMultiplier = Some(0.1)
         ),
         organizationId = "org-org123",
-        model = "curie",
+        model = FineTuneModel.Curie,
         trainingFiles = Seq(
           FileData(
             `object` = "file",
@@ -379,7 +379,7 @@ class FineTunesDataSpec extends AnyFlatSpec with Matchers with EitherValues {
       FineTuneData(
         id = "ft-xhrpBbvVUzYGo8oUO1FY4nI7",
         `object` = "fine-tune",
-        model = "curie",
+        model = FineTuneModel.Curie,
         createdAt = 1614807770,
         fineTunedModel = None,
         hyperparams = Hyperparams(
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 95fcb56..99861a3 100644
--- a/core/src/test/scala/sttp/openai/requests/moderations/ModerationsDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/moderations/ModerationsDataSpec.scala
@@ -5,6 +5,7 @@ import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 import sttp.openai.fixtures
 import sttp.openai.json.SttpUpickleApiExtension
+import sttp.openai.requests.moderations.ModerationsRequestBody.ModerationModel
 import sttp.openai.requests.moderations.ModerationsResponseData._
 
 class ModerationsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
@@ -13,7 +14,7 @@ class ModerationsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     val listFilesResponse = fixtures.ModerationsFixture.jsonCreateModerationResponse
     val expectedResponse = ModerationData(
       id = "modr-5MWoLO",
-      model = "text-moderation-001",
+      model = ModerationModel.TextModerationStable,
       results = Seq(
         Result(
           flagged = true,