Repository: softwaremill/sttp-openai
Pull request #288 (2025-03-04 07:31:49)

Extend Chat completions API


Patch:
diff --git a/core/src/main/scala/sttp/openai/OpenAI.scala b/core/src/main/scala/sttp/openai/OpenAI.scala
index d568137..b6d12a5 100644
--- a/core/src/main/scala/sttp/openai/OpenAI.scala
+++ b/core/src/main/scala/sttp/openai/OpenAI.scala
@@ -21,8 +21,15 @@ import sttp.openai.requests.audio.translations.TranslationConfig
 import sttp.openai.requests.batch.{QueryParameters => _, _}
 import sttp.openai.requests.completions.CompletionsRequestBody.CompletionsBody
 import sttp.openai.requests.completions.CompletionsResponseData.CompletionsResponse
-import sttp.openai.requests.completions.chat.ChatRequestBody.ChatBody
-import sttp.openai.requests.completions.chat.ChatRequestResponseData.ChatResponse
+import sttp.openai.requests.completions.chat
+import sttp.openai.requests.completions.chat.ChatRequestBody.{ChatBody, UpdateChatCompletionRequestBody}
+import sttp.openai.requests.completions.chat.ChatRequestResponseData.{
+  ChatResponse,
+  DeleteChatCompletionResponse,
+  ListChatResponse,
+  ListMessageResponse
+}
+import sttp.openai.requests.completions.chat.{ListMessagesQueryParameters => _}
 import sttp.openai.requests.embeddings.EmbeddingsRequestBody.EmbeddingsBody
 import sttp.openai.requests.embeddings.EmbeddingsResponseBody.EmbeddingResponse
 import sttp.openai.requests.files.FilesResponseData._
@@ -282,6 +289,100 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
       .body(ChatBody.withStreaming(chatBody))
       .response(asInputStreamUnsafe_parseErrors)
 
+  /** Get a stored chat completion. Only chat completions that have been created with the store parameter set to true will be returned.
+    *
+    * [[https://platform.openai.com/docs/api-reference/chat/get]]
+    *
+    * @param completionId
+    *   The ID of the chat completion to retrieve.
+    *
+    * @return
+    *   The ChatCompletion object matching the specified ID.
+    */
+  def getChatCompletion(completionId: String): Request[Either[OpenAIException, ChatResponse]] =
+    openAIAuthRequest
+      .get(openAIUris.chatCompletion(completionId))
+      .response(asJson_parseErrors[ChatResponse])
+
+  /** Get the messages in a stored chat completion. Only chat completions that have been created with the store parameter set to true will
+    * be returned.
+    *
+    * [[https://platform.openai.com/docs/api-reference/chat/getMessages]]
+    *
+    * @param completionId
+    *   The ID of the chat completion to retrieve messages from.
+    *
+    * @return
+    *   A list of messages for the specified chat completion.
+    */
+  def getChatMessages(
+      completionId: String,
+      queryParameters: chat.ListMessagesQueryParameters = chat.ListMessagesQueryParameters.empty
+  ): Request[Either[OpenAIException, ListMessageResponse]] = {
+    val uri = openAIUris
+      .chatMessages(completionId)
+      .withParams(queryParameters.toMap)
+
+    openAIAuthRequest
+      .get(uri)
+      .response(asJson_parseErrors[ListMessageResponse])
+  }
+
+  /** List stored chat completions. Only chat completions that have been stored with the store parameter set to true will be returned.
+    *
+    * [[https://platform.openai.com/docs/api-reference/chat/list]]
+    *
+    * @return
+    *   A list of chat completions matching the specified filters.
+    */
+  def listChatCompletions(
+      queryParameters: chat.ListChatCompletionsQueryParameters = chat.ListChatCompletionsQueryParameters.empty
+  ): Request[Either[OpenAIException, ListChatResponse]] = {
+    val uri = openAIUris.ChatCompletions
+      .withParams(queryParameters.toMap)
+
+    openAIAuthRequest
+      .get(uri)
+      .response(asJson_parseErrors[ListChatResponse])
+  }
+
+  /** Modify a stored chat completion. Only chat completions that have been created with the store parameter set to true can be modified.
+    * Currently, the only supported modification is to update the metadata field.
+    *
+    * [[https://platform.openai.com/docs/api-reference/chat/update]]
+    *
+    * @param completionId
+    *   The ID of the chat completion to update.
+    * @param requestBody
+    *   Chat completion update request body.
+    *
+    * @return
+    *   The ChatCompletion object matching the specified ID.
+    */
+  def updateChatCompletion(
+      completionId: String,
+      requestBody: UpdateChatCompletionRequestBody
+  ): Request[Either[OpenAIException, ChatResponse]] =
+    openAIAuthRequest
+      .post(openAIUris.chatCompletion(completionId))
+      .body(requestBody)
+      .response(asJson_parseErrors[ChatResponse])
+
+  /** Delete a stored chat completion. Only chat completions that have been created with the store parameter set to true can be deleted.
+    *
+    * [[https://platform.openai.com/docs/api-reference/chat/delete]]
+    *
+    * @param completionId
+    *   The ID of the chat completion to delete.
+    *
+    * @return
+    *   A deletion confirmation object.
+    */
+  def deleteChatCompletion(completionId: String): Request[Either[OpenAIException, DeleteChatCompletionResponse]] =
+    openAIAuthRequest
+      .delete(openAIUris.chatCompletion(completionId))
+      .response(asJson_parseErrors[DeleteChatCompletionResponse])
+
   /** Returns a list of files that belong to the user's organization.
     *
     * [[https://platform.openai.com/docs/api-reference/files]]
@@ -1288,6 +1389,9 @@ private class OpenAIUris(val baseUri: Uri) {
   val ThreadsRuns: Uri = uri"$baseUri/threads/runs"
   val VectorStores: Uri = uri"$baseUri/vector_stores"
 
+  def chatCompletion(completionId: String): Uri = ChatCompletions.addPath(completionId)
+  def chatMessages(completionId: String): Uri = chatCompletion(completionId).addPath("messages")
+
   def fineTuningJob(fineTuningJobId: String): Uri = FineTuningJobs.addPath(fineTuningJobId)
   def fineTuningJobEvents(fineTuningJobId: String): Uri = fineTuningJob(fineTuningJobId).addPath("events")
   def fineTuningJobCheckpoints(fineTuningJobId: String): Uri = fineTuningJob(fineTuningJobId).addPath("checkpoints")
diff --git a/core/src/main/scala/sttp/openai/OpenAISyncClient.scala b/core/src/main/scala/sttp/openai/OpenAISyncClient.scala
index 37a3c2a..67fc62b 100644
--- a/core/src/main/scala/sttp/openai/OpenAISyncClient.scala
+++ b/core/src/main/scala/sttp/openai/OpenAISyncClient.scala
@@ -13,8 +13,15 @@ import sttp.openai.requests.audio.translations.TranslationConfig
 import sttp.openai.requests.batch.{BatchRequestBody, BatchResponse, ListBatchResponse}
 import sttp.openai.requests.completions.CompletionsRequestBody.CompletionsBody
 import sttp.openai.requests.completions.CompletionsResponseData.CompletionsResponse
-import sttp.openai.requests.completions.chat.ChatRequestBody.ChatBody
-import sttp.openai.requests.completions.chat.ChatRequestResponseData.ChatResponse
+import sttp.openai.requests.completions.chat
+import sttp.openai.requests.completions.chat.ChatRequestBody.{ChatBody, UpdateChatCompletionRequestBody}
+import sttp.openai.requests.completions.chat.ChatRequestResponseData.{
+  ChatResponse,
+  DeleteChatCompletionResponse,
+  ListChatResponse,
+  ListMessageResponse
+}
+import sttp.openai.requests.completions.chat.{ListMessagesQueryParameters => _}
 import sttp.openai.requests.embeddings.EmbeddingsRequestBody.EmbeddingsBody
 import sttp.openai.requests.embeddings.EmbeddingsResponseBody.EmbeddingResponse
 import sttp.openai.requests.files.FilesResponseData.{DeletedFileData, FileData, FilesResponse}
@@ -176,6 +183,77 @@ class OpenAISyncClient private (
   def createChatCompletion(chatBody: ChatBody): ChatResponse =
     sendOrThrow(openAI.createChatCompletion(chatBody))
 
+  /** Get a stored chat completion. Only chat completions that have been created with the store parameter set to true will be returned.
+    *
+    * [[https://platform.openai.com/docs/api-reference/chat/get]]
+    *
+    * @param completionId
+    *   The ID of the chat completion to retrieve.
+    *
+    * @return
+    *   The ChatCompletion object matching the specified ID.
+    */
+  def getChatCompletion(completionId: String): ChatResponse =
+    sendOrThrow(openAI.getChatCompletion(completionId))
+
+  /** Get the messages in a stored chat completion. Only chat completions that have been created with the store parameter set to true will
+    * be returned.
+    *
+    * [[https://platform.openai.com/docs/api-reference/chat/getMessages]]
+    *
+    * @param completionId
+    *   The ID of the chat completion to retrieve messages from.
+    *
+    * @return
+    *   A list of messages for the specified chat completion.
+    */
+  def getChatMessages(
+      completionId: String,
+      queryParameters: chat.ListMessagesQueryParameters = chat.ListMessagesQueryParameters.empty
+  ): ListMessageResponse =
+    sendOrThrow(openAI.getChatMessages(completionId, queryParameters))
+
+  /** List stored chat completions. Only chat completions that have been stored with the store parameter set to true will be returned.
+    *
+    * [[https://platform.openai.com/docs/api-reference/chat/list]]
+    *
+    * @return
+    *   A list of chat completions matching the specified filters.
+    */
+  def listChatCompletions(
+      queryParameters: chat.ListChatCompletionsQueryParameters = chat.ListChatCompletionsQueryParameters.empty
+  ): ListChatResponse =
+    sendOrThrow(openAI.listChatCompletions(queryParameters))
+
+  /** Modify a stored chat completion. Only chat completions that have been created with the store parameter set to true can be modified.
+    * Currently, the only supported modification is to update the metadata field.
+    *
+    * [[https://platform.openai.com/docs/api-reference/chat/update]]
+    *
+    * @param completionId
+    *   The ID of the chat completion to update.
+    * @param requestBody
+    *   Chat completion update request body.
+    *
+    * @return
+    *   The ChatCompletion object matching the specified ID.
+    */
+  def updateChatCompletion(completionId: String, requestBody: UpdateChatCompletionRequestBody): ChatResponse =
+    sendOrThrow(openAI.updateChatCompletion(completionId, requestBody))
+
+  /** Delete a stored chat completion. Only chat completions that have been created with the store parameter set to true can be deleted.
+    *
+    * [[https://platform.openai.com/docs/api-reference/chat/delete]]
+    *
+    * @param completionId
+    *   The ID of the chat completion to delete.
+    *
+    * @return
+    *   A deletion confirmation object.
+    */
+  def deleteChatCompletion(completionId: String): DeleteChatCompletionResponse =
+    sendOrThrow(openAI.deleteChatCompletion(completionId))
+
   /** Returns a list of files that belong to the user's organization.
     *
     * [[https://platform.openai.com/docs/api-reference/files]]
diff --git a/core/src/main/scala/sttp/openai/requests/completions/chat/Audio.scala b/core/src/main/scala/sttp/openai/requests/completions/chat/Audio.scala
new file mode 100644
index 0000000..db70144
--- /dev/null
+++ b/core/src/main/scala/sttp/openai/requests/completions/chat/Audio.scala
@@ -0,0 +1,24 @@
+package sttp.openai.requests.completions.chat
+
+import sttp.openai.json.SnakePickle
+
+/** @param id
+  *   Unique identifier for this audio response.
+  * @param expiresAt
+  *   The Unix timestamp (in seconds) for when this audio response will no longer be accessible on the server for use in multi-turn
+  *   conversations.
+  * @param data
+  *   Base64 encoded audio bytes generated by the model, in the format specified in the request.
+  * @param transcript
+  *   Transcript of the audio generated by the model.
+  */
+case class Audio(
+    id: String,
+    expiresAt: Int,
+    data: String,
+    transcript: String
+)
+
+object Audio {
+  implicit val audioR: SnakePickle.Reader[Audio] = SnakePickle.macroR[Audio]
+}
diff --git a/core/src/main/scala/sttp/openai/requests/completions/chat/ChatRequestBody.scala b/core/src/main/scala/sttp/openai/requests/completions/chat/ChatRequestBody.scala
index 7b4a29e..61c9df0 100644
--- a/core/src/main/scala/sttp/openai/requests/completions/chat/ChatRequestBody.scala
+++ b/core/src/main/scala/sttp/openai/requests/completions/chat/ChatRequestBody.scala
@@ -445,4 +445,16 @@ object ChatRequestBody {
     private val byChatModelValue = values.map(model => model.value -> model).toMap
   }
 
+  /** @param metadata
+    *   Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object
+    *   in a structured format, and querying for objects via API or the dashboard. Keys are strings with a maximum length of 64 characters.
+    *   Values are strings with a maximum length of 512 characters.
+    */
+  case class UpdateChatCompletionRequestBody(metadata: Map[String, String])
+
+  object UpdateChatCompletionRequestBody {
+    implicit val updateChatCompletionRequestBodyW: SnakePickle.Writer[UpdateChatCompletionRequestBody] =
+      SnakePickle.macroW[UpdateChatCompletionRequestBody]
+  }
+
 }
diff --git a/core/src/main/scala/sttp/openai/requests/completions/chat/ChatRequestResponseData.scala b/core/src/main/scala/sttp/openai/requests/completions/chat/ChatRequestResponseData.scala
index 1b52546..5e6ff55 100644
--- a/core/src/main/scala/sttp/openai/requests/completions/chat/ChatRequestResponseData.scala
+++ b/core/src/main/scala/sttp/openai/requests/completions/chat/ChatRequestResponseData.scala
@@ -5,17 +5,58 @@ import sttp.openai.requests.completions.Usage
 
 object ChatRequestResponseData {
 
-  /** @param role
-    *   The role of the author of this message.
-    * @param content
+  /** @param content
     *   The contents of the message.
+    * @param refusal
+    *   The refusal message generated by the model.
+    * @param toolCalls
+    *   The tool calls generated by the model, such as function calls.
+    * @param role
+    *   The role of the author of this message.
     * @param functionCall
     *   The name and arguments of a function that should be called, as generated by the model.
+    * @param audio
+    *   If the audio output modality is requested, this object contains data about the audio response from the model. Learn more.
+    * @param id
+    *   The identifier of the chat message.
     */
-  case class Message(role: Role, content: String, toolCalls: Seq[ToolCall] = Nil, functionCall: Option[FunctionCall] = None)
+  case class Message(
+      content: String,
+      refusal: Option[String] = None,
+      toolCalls: Seq[ToolCall] = Nil,
+      role: Role,
+      functionCall: Option[FunctionCall] = None,
+      audio: Option[Audio] = None,
+      id: String
+  )
 
   object Message {
-    implicit val messageRW: SnakePickle.Reader[Message] = SnakePickle.macroR[Message]
+    implicit val messageR: SnakePickle.Reader[Message] = SnakePickle.macroR[Message]
+  }
+
+  /** An object representing a list of chat completion messages.
+    *
+    * @param `object`
+    *   The type of this object. It is always set to "list".
+    * @param data
+    *   An array of chat completion message objects.
+    * @param firstId
+    *   The identifier of the first chat message in the data array.
+    * @param lastId
+    *   The identifier of the last chat message in the data array.
+    * @param hasMore
+    *   Indicates whether there are more chat messages available.
+    */
+  case class ListMessageResponse(
+      `object`: String = "list",
+      data: Seq[Message],
+      firstId: String,
+      lastId: String,
+      hasMore: Boolean
+  )
+
+  object ListMessageResponse {
+    implicit val listMessageR: SnakePickle.Reader[ListMessageResponse] = SnakePickle.macroR[ListMessageResponse]
   }
 
   /** Represents a choice in the chat completion.
@@ -135,4 +176,40 @@ object ChatRequestResponseData {
     implicit val chatResponseR: SnakePickle.Reader[ChatResponse] = SnakePickle.macroR[ChatResponse]
   }
 
+  /** An object representing a list of chat completions.
+    *
+    * @param `object`
+    *   The type of this object. It is always set to "list".
+    * @param data
+    *   An array of chat completion objects.
+    * @param firstId
+    *   The identifier of the first chat completion in the data array.
+    * @param lastId
+    *   The identifier of the last chat completion in the data array.
+    * @param hasMore
+    *   Indicates whether there are more chat completions available.
+    */
+  case class ListChatResponse(
+      `object`: String = "list",
+      data: Seq[ChatResponse],
+      firstId: String,
+      lastId: String,
+      hasMore: Boolean
+  )
+
+  object ListChatResponse {
+    implicit val listChatResponseR: SnakePickle.Reader[ListChatResponse] = SnakePickle.macroR[ListChatResponse]
+  }
+
+  case class DeleteChatCompletionResponse(
+      `object`: String = "chat.completion.deleted",
+      id: String,
+      deleted: Boolean
+  )
+
+  object DeleteChatCompletionResponse {
+    implicit val deleteChatCompletionResponseR: SnakePickle.Reader[DeleteChatCompletionResponse] =
+      SnakePickle.macroR[DeleteChatCompletionResponse]
+  }
+
 }
diff --git a/core/src/main/scala/sttp/openai/requests/completions/chat/ListChatCompletionsQueryParameters.scala b/core/src/main/scala/sttp/openai/requests/completions/chat/ListChatCompletionsQueryParameters.scala
new file mode 100644
index 0000000..26914af
--- /dev/null
+++ b/core/src/main/scala/sttp/openai/requests/completions/chat/ListChatCompletionsQueryParameters.scala
@@ -0,0 +1,36 @@
+package sttp.openai.requests.completions.chat
+
+import sttp.openai.requests.completions.chat.ChatRequestBody.ChatCompletionModel
+
+/** @param model
+  *   The model used to generate the chat completions.
+  * @param metadata
+  *   A list of metadata keys to filter the chat completions by. Example: metadata[key1]=value1&metadata[key2]=value2
+  * @param after
+  *   Identifier for the last chat completion from the previous pagination request.
+  * @param limit
+  *   Number of chat completions to retrieve. Defaults to 20.
+  * @param order
+  *   Sort order for chat completions by timestamp. Use asc for ascending order or desc for descending order. Defaults to asc.
+  */
+case class ListChatCompletionsQueryParameters(
+    model: Option[ChatCompletionModel] = None,
+    metadata: Option[Map[String, String]] = None,
+    after: Option[String] = None,
+    limit: Option[Int] = None,
+    order: Option[String] = None
+) {
+
+  def toMap: Map[String, String] = {
+    val queryParams = model.map("model" -> _.value) ++
+      metadata.map(_.map { case (k, v) => s"metadata[$k]" -> v }).getOrElse(Map.empty) ++
+      after.map("after" -> _) ++
+      limit.map(_.toString).map("limit" -> _) ++
+      order.map("order" -> _)
+    queryParams.toMap
+  }
+}
+
+object ListChatCompletionsQueryParameters {
+  val empty: ListChatCompletionsQueryParameters = ListChatCompletionsQueryParameters(None, None, None, None, None)
+}
diff --git a/core/src/main/scala/sttp/openai/requests/completions/chat/ListMessagesQueryParameters.scala b/core/src/main/scala/sttp/openai/requests/completions/chat/ListMessagesQueryParameters.scala
new file mode 100644
index 0000000..5603f1f
--- /dev/null
+++ b/core/src/main/scala/sttp/openai/requests/completions/chat/ListMessagesQueryParameters.scala
@@ -0,0 +1,26 @@
+package sttp.openai.requests.completions.chat
+
+/** @param after
+  *   Identifier for the last message from the previous pagination request.
+  * @param order
+  *   Sort order for messages by timestamp. Use asc for ascending order or desc for descending order. Defaults to asc. Defaults to asc.
+  * @param limit
+  *   Number of fine-tuning jobs to retrieve. Defaults to 20.
+  */
+case class ListMessagesQueryParameters(
+    after: Option[String] = None,
+    order: Option[String] = None,
+    limit: Option[Int] = None
+) {
+
+  def toMap: Map[String, String] = {
+    val queryParams = after.map("after" -> _) ++
+      order.map("order" -> _) ++
+      limit.map(_.toString).map("limit" -> _)
+    queryParams.toMap
+  }
+}
+
+object ListMessagesQueryParameters {
+  val empty: ListMessagesQueryParameters = ListMessagesQueryParameters(None, None, None)
+}


Test patch:
diff --git a/core/src/test/scala/sttp/openai/fixtures/ChatFixture.scala b/core/src/test/scala/sttp/openai/fixtures/ChatFixture.scala
index f52d345..73e7d45 100644
--- a/core/src/test/scala/sttp/openai/fixtures/ChatFixture.scala
+++ b/core/src/test/scala/sttp/openai/fixtures/ChatFixture.scala
@@ -2,6 +2,13 @@ package sttp.openai.fixtures
 
 object ChatFixture {
 
+  val jsonUpdateRequest: String =
+    """{
+      | "metadata": {
+      |   "key": "value"
+      | }
+      |}""".stripMargin
+
   val jsonRequest: String =
     """{
       |  "messages": [
@@ -183,7 +190,8 @@ object ChatFixture {
       |             "name": "Fish"
       |           }
       |         }
-      |        ]
+      |        ],
+      |        "id": "chatcmpl-76FxnKOjnPkDVYTAQ1wK8iUNFJPvR"
       |      },
       |      "finish_reason": "stop",
       |      "index": 0,
@@ -223,4 +231,44 @@ object ChatFixture {
       |}
       |""".stripMargin
 
+  val jsonListMessageResponse: String =
+    """{
+      |  "object": "list",
+      |  "data": [
+      |    {
+      |      "role": "assistant",
+      |      "content": "Hi there! How can I assist you today?",
+      |      "tool_calls": [],
+      |      "id": "chatcmpl-76FxnKOjnPkDVYTAQ1wK8iUNFJPvR",
+      |      "audio": {
+      |        "id": "audio_id",
+      |        "expires_at": 1681725687,
+      |        "data": "base64encoded",
+      |        "transcript": "transcript"
+      |      }
+      |    }
+      |  ],
+      |  "first_id": "chatcmpl-76FxnKOjnPkDVYTAQ1wK8iUNFJPvR",
+      |  "last_id": "chatcmpl-76FxnKOjnPkDVYTAQ1wK8iUNFJPvR",
+      |  "has_more": true
+      |}
+      |""".stripMargin
+
+  val jsonListChatResponse: String =
+    """{
+      |  "object": "list",
+      |  "data": [],
+      |  "first_id": "chatcmpl-AyPNinnUqUDYo9SAdA52NobMflmj2",
+      |  "last_id": "chatcmpl-AyPNinnUqUDYo9SAdA52NobMflmj2",
+      |  "has_more": false
+      |}
+      |""".stripMargin
+
+  val jsonDeleteChatCompletionResponse: String =
+    """{
+      |  "object": "chat.completion.deleted",
+      |  "id": "chatcmpl-AyPNinnUqUDYo9SAdA52NobMflmj2",
+      |  "deleted": true
+      |}""".stripMargin
+
 }
diff --git a/core/src/test/scala/sttp/openai/requests/completions/chat/ChatDataSpec.scala b/core/src/test/scala/sttp/openai/requests/completions/chat/ChatDataSpec.scala
index f0950b2..6ec0a0d 100644
--- a/core/src/test/scala/sttp/openai/requests/completions/chat/ChatDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/completions/chat/ChatDataSpec.scala
@@ -8,11 +8,78 @@ import sttp.openai.json.{SnakePickle, SttpUpickleApiExtension}
 import sttp.openai.requests.completions.Stop.SingleStop
 import sttp.openai.requests.completions.chat.ChatRequestBody.Format.Mp3
 import sttp.openai.requests.completions.chat.ChatRequestBody.Voice.Ash
+import sttp.openai.requests.completions.chat.Role.Assistant
 import sttp.openai.requests.completions.{CompletionTokensDetails, PromptTokensDetails, Usage}
 import sttp.openai.utils.ChatCompletionFixtures._
 
 class ChatDataSpec extends AnyFlatSpec with Matchers with EitherValues {
 
+  "Given delete chat completion response as Json" should "be properly deserialized to case class" in {
+    import ChatRequestResponseData.DeleteChatCompletionResponse
+    import ChatRequestResponseData.DeleteChatCompletionResponse._
+
+    // given
+    val jsonResponse = fixtures.ChatFixture.jsonDeleteChatCompletionResponse
+    val expectedResponse = DeleteChatCompletionResponse(
+      id = "chatcmpl-AyPNinnUqUDYo9SAdA52NobMflmj2",
+      deleted = true
+    )
+    // when
+    val givenResponse: Either[Exception, DeleteChatCompletionResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    // then
+    givenResponse.value shouldBe expectedResponse
+  }
+
+  "Given list chat response as Json" should "be properly deserialized to case class" in {
+    import ChatRequestResponseData.ListChatResponse
+    import ChatRequestResponseData.ListChatResponse._
+
+    // given
+    val jsonResponse = fixtures.ChatFixture.jsonListChatResponse
+    val expectedResponse = ListChatResponse(
+      data = Seq(),
+      firstId = "chatcmpl-AyPNinnUqUDYo9SAdA52NobMflmj2",
+      lastId = "chatcmpl-AyPNinnUqUDYo9SAdA52NobMflmj2",
+      hasMore = false
+    )
+    // when
+    val givenResponse: Either[Exception, ListChatResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    // then
+    givenResponse.value shouldBe expectedResponse
+  }
+
+  "Given list message response as Json" should "be properly deserialized to case class" in {
+    import ChatRequestResponseData.ListMessageResponse._
+    import ChatRequestResponseData.{ListMessageResponse, Message}
+
+    // given
+    val jsonResponse = fixtures.ChatFixture.jsonListMessageResponse
+    val expectedResponse = ListMessageResponse(
+      data = Seq(
+        Message(
+          role = Assistant,
+          content = "Hi there! How can I assist you today?",
+          id = "chatcmpl-76FxnKOjnPkDVYTAQ1wK8iUNFJPvR",
+          audio = Some(
+            Audio(
+              id = "audio_id",
+              expiresAt = 1681725687,
+              data = "base64encoded",
+              transcript = "transcript"
+            )
+          )
+        )
+      ),
+      firstId = "chatcmpl-76FxnKOjnPkDVYTAQ1wK8iUNFJPvR",
+      lastId = "chatcmpl-76FxnKOjnPkDVYTAQ1wK8iUNFJPvR",
+      hasMore = true
+    )
+    // when
+    val givenResponse: Either[Exception, ListMessageResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    // then
+    givenResponse.value shouldBe expectedResponse
+  }
+
   "Given chat completions response as Json" should "be properly deserialized to case class" in {
     import ChatRequestResponseData.ChatResponse._
     import ChatRequestResponseData._
@@ -36,7 +103,8 @@ class ChatDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     val message: Message = Message(
       role = Role.Assistant,
       content = "Hi there! How can I assist you today?",
-      toolCalls = toolCalls
+      toolCalls = toolCalls,
+      id = "chatcmpl-76FxnKOjnPkDVYTAQ1wK8iUNFJPvR"
     )
 
     val choices: Choices = Choices(
@@ -129,4 +197,18 @@ class ChatDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     // then
     serializedJson shouldBe jsonRequest
   }
+
+  "Given update chat completions request as case class" should "be properly serialized to Json" in {
+    import ChatRequestBody.UpdateChatCompletionRequestBody._
+
+    // given
+    val givenRequest = ChatRequestBody.UpdateChatCompletionRequestBody(
+      metadata = Map("key" -> "value")
+    )
+    val jsonRequest: ujson.Value = ujson.read(fixtures.ChatFixture.jsonUpdateRequest)
+    // when
+    val serializedJson = SnakePickle.writeJs(givenRequest)
+    // then
+    serializedJson shouldBe jsonRequest
+  }
 }