Repository: softwaremill/sttp-openai (scala)

Issue:
Delete fine-tune model
Given uri:
`https://api.openai.com/v1/models/{model}`

Create sttp DELETE endpoint + tests

Docs reference
https://platform.openai.com/docs/api-reference/fine-tunes/delete-model

Pull request:
Closes #21

Patch:
diff --git a/core/src/main/scala/sttp/openai/OpenAi.scala b/core/src/main/scala/sttp/openai/OpenAi.scala
index 5ee4229d..4b6d0650 100644
--- a/core/src/main/scala/sttp/openai/OpenAi.scala
+++ b/core/src/main/scala/sttp/openai/OpenAi.scala
@@ -13,7 +13,12 @@ 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.FineTunesResponseData.{FineTuneEventsResponse, FineTuneResponse, GetFineTunesResponse}
+import sttp.openai.requests.finetunes.FineTunesResponseData.{
+  DeleteFineTuneModelResponse,
+  FineTuneEventsResponse,
+  FineTuneResponse,
+  GetFineTunesResponse
+}
 import sttp.openai.requests.images.ImageResponseData.ImageResponse
 import sttp.openai.requests.images.creation.ImageCreationRequestBody.ImageCreationBody
 import sttp.openai.requests.images.edit.ImageEditsConfig
@@ -534,6 +539,18 @@ class OpenAi(authToken: String) {
       .get(OpenAIEndpoints.retrieveFineTuneEndpoint(fineTuneId))
       .response(asJsonSnake[FineTuneResponse])
 
+  /** Delete a fine-tuned model. You must have the Owner role in your organization.
+    *
+    * @param model
+    *   The model to delete.
+    * @return
+    *   Deleted fine-tuned model information.
+    */
+  def deleteFineTuneModel(model: String): Request[Either[ResponseException[String, Exception], DeleteFineTuneModelResponse]] =
+    openApiAuthRequest
+      .delete(OpenAIEndpoints.deleteFineTuneModelUri(model))
+      .response(asJsonSnake[DeleteFineTuneModelResponse])
+
   /** @param fineTuneId
     *   The ID of the fine-tune job to get events for.
     * @return
@@ -568,6 +585,7 @@ private object OpenAIEndpoints {
 
   def cancelFineTuneEndpoint(fineTuneId: String): Uri = FineTunesEndpoint.addPath(fineTuneId, "cancel")
   def deleteFileEndpoint(fileId: String): Uri = FilesEndpoint.addPath(fileId)
+  def deleteFineTuneModelUri(model: String): Uri = ModelEndpoint.addPath(model)
   def listFineTunesEndpoint(fineTuneId: String): Uri = FineTunesEndpoint.addPath(fineTuneId, "events")
   def retrieveFileEndpoint(fileId: String): Uri = FilesEndpoint.addPath(fileId)
   def retrieveFineTuneEndpoint(fineTuneId: String): Uri = FineTunesEndpoint.addPath(fineTuneId)
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 1ed4a351..5ca0edc9 100644
--- a/core/src/main/scala/sttp/openai/requests/finetunes/FineTunesResponseData.scala
+++ b/core/src/main/scala/sttp/openai/requests/finetunes/FineTunesResponseData.scala
@@ -60,6 +60,10 @@ object FineTunesResponseData {
     implicit val fineTuneDataReader: SnakePickle.Reader[FineTuneData] = SnakePickle.macroR[FineTuneData]
   }
 
+  case class DeleteFineTuneModelResponse(id: String, `object`: String, deleted: Boolean)
+  object DeleteFineTuneModelResponse {
+    implicit val fineTuneDataReader: SnakePickle.Reader[DeleteFineTuneModelResponse] = SnakePickle.macroR[DeleteFineTuneModelResponse]
+  }
   case class FineTuneEventsResponse(`object`: String, data: Seq[Event])
   object FineTuneEventsResponse {
     implicit val fineTuneDataReader: SnakePickle.Reader[FineTuneEventsResponse] = SnakePickle.macroR[FineTuneEventsResponse]


Test patch:
diff --git a/core/src/test/scala/sttp/openai/fixtures/FineTunesFixture.scala b/core/src/test/scala/sttp/openai/fixtures/FineTunesFixture.scala
index ae91b6f2..d319af3b 100644
--- a/core/src/test/scala/sttp/openai/fixtures/FineTunesFixture.scala
+++ b/core/src/test/scala/sttp/openai/fixtures/FineTunesFixture.scala
@@ -218,6 +218,12 @@ object FineTunesFixture {
                                        |}
                                        |""".stripMargin
 
+  val jsonDeleteFineTuneModel: String = """{
+                                          |  "id": "curie:ft-personal-2023-04-18-09-48-38",
+                                          |  "object": "model",
+                                          |  "deleted": true
+                                          |}""".stripMargin
+
   val jsonListFineTuneEventsResponse: String = """{
                                     |  "object": "list",
                                     |  "data": [
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 8af0b0d0..1d8a0719 100644
--- a/core/src/test/scala/sttp/openai/requests/finetunes/FineTunesDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/finetunes/FineTunesDataSpec.scala
@@ -270,6 +270,23 @@ class FineTunesDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     deserializedJsonResponse.value shouldBe expectedResponse
   }
 
+  "Given delete fine tune model response as Json" should "be properly deserialized to case class" in {
+
+    // given
+    val jsonResponse = fixtures.FineTunesFixture.jsonDeleteFineTuneModel
+    val expectedResponse: DeleteFineTuneModelResponse = DeleteFineTuneModelResponse(
+      id = "curie:ft-personal-2023-04-18-09-48-38",
+      `object` = "model",
+      deleted = true
+    )
+    // when
+    val deserializedJsonResponse: Either[Exception, DeleteFineTuneModelResponse] =
+      SttpUpickleApiExtension.deserializeJsonSnake[DeleteFineTuneModelResponse].apply(jsonResponse)
+
+    // then
+    deserializedJsonResponse.value shouldBe expectedResponse
+  }
+
   "Given list fine tunes events response as Json" should "be properly deserialized to case class" in {
 
     // given