Repository: softwaremill/sttp-openai
Pull request #285 (2025-02-28 09:41:16)

add Batch API, extend chat completion request body & response


Patch:
diff --git a/core/src/main/scala/sttp/openai/OpenAI.scala b/core/src/main/scala/sttp/openai/OpenAI.scala
index 953495c..4189962 100644
--- a/core/src/main/scala/sttp/openai/OpenAI.scala
+++ b/core/src/main/scala/sttp/openai/OpenAI.scala
@@ -17,6 +17,7 @@ import sttp.openai.requests.audio.AudioResponseData.AudioResponse
 import sttp.openai.requests.audio.RecognitionModel
 import sttp.openai.requests.audio.transcriptions.TranscriptionConfig
 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
@@ -24,7 +25,6 @@ import sttp.openai.requests.completions.chat.ChatRequestResponseData.ChatRespons
 import sttp.openai.requests.embeddings.EmbeddingsRequestBody.EmbeddingsBody
 import sttp.openai.requests.embeddings.EmbeddingsResponseBody.EmbeddingResponse
 import sttp.openai.requests.files.FilesResponseData._
-import sttp.openai.requests.finetuning
 import sttp.openai.requests.finetuning._
 import sttp.openai.requests.images.ImageResponseData.ImageResponse
 import sttp.openai.requests.images.creation.ImageCreationRequestBody.ImageCreationBody
@@ -37,7 +37,7 @@ import sttp.openai.requests.threads.QueryParameters
 import sttp.openai.requests.threads.ThreadsRequestBody.CreateThreadBody
 import sttp.openai.requests.threads.ThreadsResponseData.{DeleteThreadResponse, ThreadData}
 import sttp.openai.requests.threads.messages.ThreadMessagesRequestBody.CreateMessage
-import sttp.openai.requests.threads.messages.ThreadMessagesResponseData.{ListMessagesResponse, MessageData}
+import sttp.openai.requests.threads.messages.ThreadMessagesResponseData.{DeleteMessageResponse, ListMessagesResponse, MessageData}
 import sttp.openai.requests.threads.runs.ThreadRunsRequestBody._
 import sttp.openai.requests.threads.runs.ThreadRunsResponseData.{ListRunStepsResponse, ListRunsResponse, RunData, RunStepData}
 import sttp.openai.requests.vectorstore.VectorStoreRequestBody.{CreateVectorStoreBody, ModifyVectorStoreBody}
@@ -48,6 +48,7 @@ import sttp.openai.requests.vectorstore.file.VectorStoreFileResponseData.{
   ListVectorStoreFilesResponse,
   VectorStoreFile
 }
+import sttp.openai.requests.{batch, finetuning}
 
 import java.io.{File, InputStream}
 import java.nio.file.Paths
@@ -463,7 +464,8 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
           Some(multipart("model", model.value)),
           prompt.map(multipart("prompt", _)),
           responseFormat.map(format => multipart("response_format", format)),
-          temperature.map(multipart("temperature", _))
+          temperature.map(multipart("temperature", _)),
+          language.map(multipart("language", _))
         ).flatten
       }
       .response(asJson_parseErrors[AudioResponse])
@@ -767,6 +769,24 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
       .body(metadata)
       .response(asJson_parseErrors[MessageData])
 
+  /** Deletes a message.
+    *
+    * [[https://platform.openai.com/docs/api-reference/messages/deleteMessage]]
+    *
+    * @param threadId
+    *   The ID of the thread to which this message belongs.
+    *
+    * @param messageId
+    *   The ID of the message to delete.
+    *
+    * @return
+    *   Deletion status
+    */
+  def deleteMessage(threadId: String, messageId: String): Request[Either[OpenAIException, DeleteMessageResponse]] =
+    betaOpenAIAuthRequest
+      .delete(openAIUris.threadMessage(threadId, messageId))
+      .response(asJson_parseErrors[DeleteMessageResponse])
+
   /** Create an assistant with a model and instructions.
     *
     * [[https://platform.openai.com/docs/api-reference/assistants/createAssistant]]
@@ -1113,6 +1133,68 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
       .delete(openAIUris.vectorStoreFile(vectorStoreId, fileId))
       .response(asJson_parseErrors[DeleteVectorStoreFileResponse])
 
+  /** Creates and executes a batch from an uploaded file of requests
+    *
+    * [[https://platform.openai.com/docs/api-reference/batch/create]]
+    *
+    * @param createBatchRequest
+    *   Request body that will be used to create a batch.
+    * @return
+    *   The created Batch object.
+    */
+  def createBatch(createBatchRequest: BatchRequestBody): Request[Either[OpenAIException, BatchResponse]] =
+    openAIAuthRequest
+      .post(openAIUris.Batches)
+      .body(createBatchRequest)
+      .response(asJson_parseErrors[BatchResponse])
+
+  /** Retrieves a batch.
+    *
+    * [[https://platform.openai.com/docs/api-reference/batch/retreive]]
+    *
+    * @param batchId
+    *   The ID of the batch to retrieve.
+    * @return
+    *   The Batch object matching the specified ID.
+    */
+  def retrieveBatch(batchId: String): Request[Either[OpenAIException, BatchResponse]] =
+    openAIAuthRequest
+      .get(openAIUris.batch(batchId))
+      .response(asJson_parseErrors[BatchResponse])
+
+  /** Cancels an in-progress batch. The batch will be in status cancelling for up to 10 minutes, before changing to cancelled, where it will
+    * have partial results (if any) available in the output file.
+    *
+    * [[https://platform.openai.com/docs/api-reference/batch/cancel]]
+    *
+    * @param batchId
+    *   The ID of the batch to cancel.
+    * @return
+    *   The Batch object matching the specified ID.
+    */
+  def cancelBatch(batchId: String): Request[Either[OpenAIException, BatchResponse]] =
+    openAIAuthRequest
+      .post(openAIUris.cancelBatch(batchId))
+      .response(asJson_parseErrors[BatchResponse])
+
+  /** List your organization's batches.
+    *
+    * [[https://platform.openai.com/docs/api-reference/batch/list]]
+    *
+    * @return
+    *   A list of paginated Batch objects.
+    */
+  def listBatches(
+      queryParameters: batch.QueryParameters = batch.QueryParameters.empty
+  ): Request[Either[OpenAIException, ListBatchResponse]] = {
+    val uri = openAIUris.Batches
+      .withParams(queryParameters.toMap)
+
+    openAIAuthRequest
+      .get(uri)
+      .response(asJson_parseErrors[ListBatchResponse])
+  }
+
   protected val openAIAuthRequest: PartialRequest[Either[String, String]] = basicRequest.auth
     .bearer(authToken)
 
@@ -1133,6 +1215,7 @@ private class OpenAIUris(val baseUri: Uri) {
   val Models: Uri = uri"$baseUri/models"
   val Moderations: Uri = uri"$baseUri/moderations"
   val FineTuningJobs: Uri = uri"$baseUri/fine_tuning/jobs"
+  val Batches: Uri = uri"$baseUri/batches"
   val Transcriptions: Uri = audioBase.addPath("transcriptions")
   val Translations: Uri = audioBase.addPath("translations")
   val VariationsImage: Uri = imageBase.addPath("variations")
@@ -1147,6 +1230,9 @@ private class OpenAIUris(val baseUri: Uri) {
   def fineTuningJobCheckpoints(fineTuningJobId: String): Uri = fineTuningJob(fineTuningJobId).addPath("checkpoints")
   def cancelFineTuningJob(fineTuningJobId: String): Uri = fineTuningJob(fineTuningJobId).addPath("cancel")
 
+  def batch(batchId: String): Uri = Batches.addPath(batchId)
+  def cancelBatch(batchId: String): Uri = batch(batchId).addPath("cancel")
+
   def file(fileId: String): Uri = Files.addPath(fileId)
   def fileContent(fileId: String): Uri = Files.addPath(fileId, "content")
   def model(modelId: String): Uri = Models.addPath(modelId)
diff --git a/core/src/main/scala/sttp/openai/OpenAISyncClient.scala b/core/src/main/scala/sttp/openai/OpenAISyncClient.scala
index 78bee40..a8ed393 100644
--- a/core/src/main/scala/sttp/openai/OpenAISyncClient.scala
+++ b/core/src/main/scala/sttp/openai/OpenAISyncClient.scala
@@ -9,6 +9,7 @@ import sttp.openai.requests.audio.AudioResponseData.AudioResponse
 import sttp.openai.requests.audio.RecognitionModel
 import sttp.openai.requests.audio.transcriptions.TranscriptionConfig
 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
@@ -16,7 +17,6 @@ import sttp.openai.requests.completions.chat.ChatRequestResponseData.ChatRespons
 import sttp.openai.requests.embeddings.EmbeddingsRequestBody.EmbeddingsBody
 import sttp.openai.requests.embeddings.EmbeddingsResponseBody.EmbeddingResponse
 import sttp.openai.requests.files.FilesResponseData.{DeletedFileData, FileData, FilesResponse}
-import sttp.openai.requests.finetuning
 import sttp.openai.requests.finetuning._
 import sttp.openai.requests.images.ImageResponseData.ImageResponse
 import sttp.openai.requests.images.creation.ImageCreationRequestBody.ImageCreationBody
@@ -29,7 +29,7 @@ import sttp.openai.requests.threads.QueryParameters
 import sttp.openai.requests.threads.ThreadsRequestBody.CreateThreadBody
 import sttp.openai.requests.threads.ThreadsResponseData.{DeleteThreadResponse, ThreadData}
 import sttp.openai.requests.threads.messages.ThreadMessagesRequestBody.CreateMessage
-import sttp.openai.requests.threads.messages.ThreadMessagesResponseData.{ListMessagesResponse, MessageData}
+import sttp.openai.requests.threads.messages.ThreadMessagesResponseData.{DeleteMessageResponse, ListMessagesResponse, MessageData}
 import sttp.openai.requests.threads.runs.ThreadRunsRequestBody.{CreateRun, CreateThreadAndRun, ToolOutput}
 import sttp.openai.requests.threads.runs.ThreadRunsResponseData.{ListRunStepsResponse, ListRunsResponse, RunData, RunStepData}
 import sttp.openai.requests.vectorstore.VectorStoreRequestBody.{CreateVectorStoreBody, ModifyVectorStoreBody}
@@ -40,6 +40,7 @@ import sttp.openai.requests.vectorstore.file.VectorStoreFileResponseData.{
   ListVectorStoreFilesResponse,
   VectorStoreFile
 }
+import sttp.openai.requests.{batch, finetuning}
 
 import java.io.File
 
@@ -517,6 +518,22 @@ class OpenAISyncClient private (
   def modifyMessage(threadId: String, messageId: String, metadata: Map[String, String]): MessageData =
     sendOrThrow(openAI.modifyMessage(threadId, messageId, metadata))
 
+  /** Deletes a message.
+    *
+    * [[https://platform.openai.com/docs/api-reference/messages/deleteMessage]]
+    *
+    * @param threadId
+    *   The ID of the thread to which this message belongs.
+    *
+    * @param messageId
+    *   The ID of the message to delete.
+    *
+    * @return
+    *   Deletion status
+    */
+  def deleteMessage(threadId: String, messageId: String): DeleteMessageResponse =
+    sendOrThrow(openAI.deleteMessage(threadId, messageId))
+
   /** Create an assistant with a model and instructions.
     *
     * [[https://platform.openai.com/docs/api-reference/assistants/createAssistant]]
@@ -798,6 +815,53 @@ class OpenAISyncClient private (
   def deleteVectorStoreFile(vectorStoreId: String, fileId: String): DeleteVectorStoreFileResponse =
     sendOrThrow(openAI.deleteVectorStoreFile(vectorStoreId, fileId))
 
+  /** Creates and executes a batch from an uploaded file of requests
+    *
+    * [[https://platform.openai.com/docs/api-reference/batch/create]]
+    *
+    * @param createBatchRequest
+    *   Request body that will be used to create a batch.
+    * @return
+    *   The created Batch object.
+    */
+  def createBatch(createBatchRequest: BatchRequestBody): BatchResponse =
+    sendOrThrow(openAI.createBatch(createBatchRequest))
+
+  /** Retrieves a batch.
+    *
+    * [[https://platform.openai.com/docs/api-reference/batch/retreive]]
+    *
+    * @param batchId
+    *   The ID of the batch to retrieve.
+    * @return
+    *   The Batch object matching the specified ID.
+    */
+  def retrieveBatch(batchId: String): BatchResponse =
+    sendOrThrow(openAI.retrieveBatch(batchId))
+
+  /** Cancels an in-progress batch. The batch will be in status cancelling for up to 10 minutes, before changing to cancelled, where it will
+    * have partial results (if any) available in the output file.
+    *
+    * [[https://platform.openai.com/docs/api-reference/batch/cancel]]
+    *
+    * @param batchId
+    *   The ID of the batch to cancel.
+    * @return
+    *   The Batch object matching the specified ID.
+    */
+  def cancelBatch(batchId: String): BatchResponse =
+    sendOrThrow(openAI.cancelBatch(batchId))
+
+  /** List your organization's batches.
+    *
+    * [[https://platform.openai.com/docs/api-reference/batch/list]]
+    *
+    * @return
+    *   A list of paginated Batch objects.
+    */
+  def listBatches(queryParameters: batch.QueryParameters = batch.QueryParameters.empty): ListBatchResponse =
+    sendOrThrow(openAI.listBatches(queryParameters))
+
   /** Closes and releases resources of http client if was not provided explicitly, otherwise works no-op. */
   def close(): Unit = if (closeClient) backend.close() else ()
 
diff --git a/core/src/main/scala/sttp/openai/requests/assistants/AssistantsRequestBody.scala b/core/src/main/scala/sttp/openai/requests/assistants/AssistantsRequestBody.scala
index 60dfbd5..c255a47 100644
--- a/core/src/main/scala/sttp/openai/requests/assistants/AssistantsRequestBody.scala
+++ b/core/src/main/scala/sttp/openai/requests/assistants/AssistantsRequestBody.scala
@@ -8,27 +8,31 @@ object AssistantsRequestBody {
   /** @param model
     *   ID of the model to use. You can use the List models API to see all of your available models, or see our Model overview for
     *   descriptions of them.
-    *
     * @param name
     *   The name of the assistant. The maximum length is 256 characters.
-    *
     * @param description
     *   The description of the assistant. The maximum length is 512 characters.
-    *
     * @param instructions
     *   The system instructions that the assistant uses. The maximum length is 32768 characters.
-    *
+    * @param reasoningEffort
+    *   o1 and o3-mini models only Constrains effort on reasoning for reasoning models. Currently supported values are low, medium, and
+    *   high. Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response.
     * @param tools
     *   A list of tool enabled on the assistant. There can be a maximum of 128 tools per assistant. Tools can be of types code_interpreter,
     *   file_search, or function.
-    *
     * @param toolResources
     *   A set of resources that are used by the assistant's tools. The resources are specific to the type of tool. For example, the
     *   code_interpreter tool requires a list of file IDs, while the file_search tool requires a list of vector store IDs.
-    *
     * @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. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long.
+    * @param temperature
+    *   What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like
+    *   0.2 will make it more focused and deterministic.
+    * @param topP
+    *   An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p
+    *   probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend
+    *   altering this or temperature but not both.
     *
     * For more information please visit: [[https://platform.openai.com/docs/api-reference/assistants/createAssistant]]
     */
@@ -37,9 +41,12 @@ object AssistantsRequestBody {
       name: Option[String] = None,
       description: Option[String] = None,
       instructions: Option[String] = None,
+      reasoningEffort: Option[ReasoningEffort] = None,
       tools: Seq[Tool] = Seq.empty,
       toolResources: Option[ToolResources] = None,
-      metadata: Option[Map[String, String]] = None
+      metadata: Option[Map[String, String]] = None,
+      temperature: Option[Float] = None,
+      topP: Option[Float] = None
   )
   object CreateAssistantBody {
     implicit val createAssistantBodyW: SnakePickle.Writer[CreateAssistantBody] = SnakePickle.macroW[CreateAssistantBody]
@@ -48,26 +55,33 @@ object AssistantsRequestBody {
   /** @param model
     *   ID of the model to use. You can use the List models API to see all of your available models, or see our Model overview for
     *   descriptions of them.
-    *
     * @param name
     *   The name of the assistant. The maximum length is 256 characters.
-    *
     * @param description
     *   The description of the assistant. The maximum length is 512 characters.
-    *
     * @param instructions
     *   The system instructions that the assistant uses. The maximum length is 32768 characters.
+    * @param reasoningEffort
+    *   o1 and o3-mini models only
     *
+    * Constrains effort on reasoning for reasoning models. Currently supported values are low, medium, and high. Reducing reasoning effort
+    * can result in faster responses and fewer tokens used on reasoning in a response.
     * @param tools
     *   A list of tool enabled on the assistant. There can be a maximum of 128 tools per assistant. Tools can be of types code_interpreter,
     *   file_search, or function.
-    *
     * @param toolResources
     *   A set of resources that are used by the assistant's tools. The resources are specific to the type of tool. For example, the
     *   code_interpreter tool requires a list of file IDs, while the file_search tool requires a list of vector store IDs. v
     * @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. Keys can be a maximum of 64 characters long and values can be a maxium of 512 characters long.
+    * @param temperature
+    *   What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like
+    *   0.2 will make it more focused and deterministic.
+    * @param topP
+    *   An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p
+    *   probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend
+    *   altering this or temperature but not both.
     *
     * For more information please visit: [[https://platform.openai.com/docs/api-reference/assistants/modifyAssistant]]
     */
@@ -76,12 +90,33 @@ object AssistantsRequestBody {
       name: Option[String] = None,
       description: Option[String] = None,
       instructions: Option[String] = None,
+      reasoningEffort: Option[ReasoningEffort] = None,
       tools: Seq[Tool] = Seq.empty,
       toolResources: Option[ToolResources] = None,
-      metadata: Map[String, String] = Map.empty
+      metadata: Map[String, String] = Map.empty,
+      temperature: Option[Float] = None,
+      topP: Option[Float] = None
   )
 
   object ModifyAssistantBody {
     implicit val modifyAssistantBodyW: SnakePickle.Writer[ModifyAssistantBody] = SnakePickle.macroW[ModifyAssistantBody]
   }
 }
+
+sealed abstract class ReasoningEffort(val value: String)
+
+object ReasoningEffort {
+
+  implicit val reasoningEffortW: SnakePickle.Writer[ReasoningEffort] = SnakePickle
+    .writer[ujson.Value]
+    .comap[ReasoningEffort](reasoningEffort => SnakePickle.writeJs(reasoningEffort.value))
+
+  case object Low extends ReasoningEffort("low")
+
+  case object Medium extends ReasoningEffort("medium")
+
+  case object High extends ReasoningEffort("high")
+
+  case class CustomReasoningEffort(customReasoningEffort: String) extends ReasoningEffort(customReasoningEffort)
+
+}
diff --git a/core/src/main/scala/sttp/openai/requests/audio/translations/TranslationConfig.scala b/core/src/main/scala/sttp/openai/requests/audio/translations/TranslationConfig.scala
index d2f45bd..df25d9e 100644
--- a/core/src/main/scala/sttp/openai/requests/audio/translations/TranslationConfig.scala
+++ b/core/src/main/scala/sttp/openai/requests/audio/translations/TranslationConfig.scala
@@ -4,7 +4,6 @@ import sttp.openai.requests.audio.RecognitionModel
 import sttp.openai.requests.images.ResponseFormat
 
 import java.io.File
-import java.nio.file.Paths
 
 /** @param file
   *   The audio file to translate, in one of these formats: mp3, mp4, mpeg, mpga, m4a, wav, or webm.
@@ -18,28 +17,14 @@ import java.nio.file.Paths
   *   The sampling temperature, between 0 and 1. Higher values like 0.8 will make the output more random, while lower values like 0.2 will
   *   make it more focused and deterministic. If set to 0, the model will use log probability to automatically increase the temperature
   *   until certain thresholds are hit.
+  * @param language
+  *   The language of the input audio. Supplying the input language in ISO-639-1 (e.g. en) format will improve accuracy and latency.
   */
 case class TranslationConfig(
     file: File,
     model: RecognitionModel,
     prompt: Option[String] = None,
     responseFormat: Option[ResponseFormat] = None,
-    temperature: Option[Float] = None
+    temperature: Option[Float] = None,
+    language: Option[String] = None
 )
-
-object TranslationConfig {
-  def createTranslationConfigWithSystemPaths(
-      systemPath: String,
-      model: RecognitionModel,
-      prompt: Option[String] = None,
-      responseFormat: Option[ResponseFormat] = None,
-      temperature: Option[Float] = None
-  ): TranslationConfig =
-    TranslationConfig(
-      Paths.get(systemPath).toFile,
-      model,
-      prompt,
-      responseFormat,
-      temperature
-    )
-}
diff --git a/core/src/main/scala/sttp/openai/requests/batch/BatchRequestBody.scala b/core/src/main/scala/sttp/openai/requests/batch/BatchRequestBody.scala
new file mode 100644
index 0000000..72560ea
--- /dev/null
+++ b/core/src/main/scala/sttp/openai/requests/batch/BatchRequestBody.scala
@@ -0,0 +1,28 @@
+package sttp.openai.requests.batch
+
+import sttp.openai.json.SnakePickle
+
+/** @param inputFileId
+  *   The ID of an uploaded file that contains requests for the new batch. Your input file must be formatted as a JSONL file, and must be
+  *   uploaded with the purpose batch. The file can contain up to 50,000 requests, and can be up to 200 MB in size.
+  * @param endpoint
+  *   The endpoint to be used for all requests in the batch. Currently, /v1/chat/completions, /v1/embeddings, and /v1/completions are
+  *   supported. Note that /v1/embeddings batches are also restricted to a maximum of 50,000 embedding inputs across all requests in the
+  *   batch.
+  * @param completionWindow
+  *   The time frame within which the batch should be processed. Currently only 24h is supported.
+  * @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 BatchRequestBody(
+    inputFileId: String,
+    endpoint: String,
+    completionWindow: String,
+    metadata: Option[Map[String, String]] = None
+)
+
+object BatchRequestBody {
+  implicit val batchRequestBodyW: SnakePickle.Writer[BatchRequestBody] = SnakePickle.macroW[BatchRequestBody]
+}
diff --git a/core/src/main/scala/sttp/openai/requests/batch/BatchResponse.scala b/core/src/main/scala/sttp/openai/requests/batch/BatchResponse.scala
new file mode 100644
index 0000000..baa55b5
--- /dev/null
+++ b/core/src/main/scala/sttp/openai/requests/batch/BatchResponse.scala
@@ -0,0 +1,131 @@
+package sttp.openai.requests.batch
+
+import sttp.openai.json.SnakePickle
+
+/** @param id
+  *   The ID of the batch.
+  * @param `object`
+  *   The object type, which is always batch.
+  * @param endpoint
+  *   The OpenAI API endpoint used by the batch.
+  * @param errors
+  *   The errors object.
+  * @param inputFileId
+  *   The ID of the input file for the batch.
+  * @param completionWindow
+  *   The time frame within which the batch should be processed.
+  * @param status
+  *   The current status of the batch.
+  * @param outputFileId
+  *   The ID of the file containing the outputs of successfully executed requests.
+  * @param errorFileId
+  *   The ID of the file containing the outputs of requests with errors.
+  * @param createdAt
+  *   The Unix timestamp (in seconds) for when the batch was created.
+  * @param inProgressAt
+  *   The Unix timestamp (in seconds) for when the batch started processing.
+  * @param expiresAt
+  *   The Unix timestamp (in seconds) for when the batch will expire.
+  * @param finalizingAt
+  *   The Unix timestamp (in seconds) for when the batch started finalizing.
+  * @param completedAt
+  *   The Unix timestamp (in seconds) for when the batch was completed.
+  * @param failedAt
+  *   The Unix timestamp (in seconds) for when the batch failed.
+  * @param expiredAt
+  *   The Unix timestamp (in seconds) for when the batch expired.
+  * @param cancellingAt
+  *   The Unix timestamp (in seconds) for when the batch started cancelling.
+  * @param cancelledAt
+  *   The Unix timestamp (in seconds) for when the batch was cancelled.
+  * @param requestCounts
+  *   The request counts for different statuses within the batch.
+  * @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 BatchResponse(
+    id: String,
+    `object`: String = "batch",
+    endpoint: String,
+    errors: Option[Errors],
+    inputFileId: String,
+    completionWindow: String,
+    status: String,
+    outputFileId: Option[String],
+    errorFileId: Option[String],
+    createdAt: Int,
+    inProgressAt: Option[Int],
+    expiresAt: Option[Int],
+    finalizingAt: Option[Int],
+    completedAt: Option[Int],
+    failedAt: Option[Int],
+    expiredAt: Option[Int],
+    cancellingAt: Option[Int],
+    cancelledAt: Option[Int],
+    requestCounts: Option[RequestCounts],
+    metadata: Option[Map[String, String]]
+)
+
+object BatchResponse {
+  implicit val batchResponseR: SnakePickle.Reader[BatchResponse] = SnakePickle.macroR[BatchResponse]
+}
+
+case class Errors(
+    `object`: String = "list",
+    data: Seq[Data]
+)
+
+object Errors {
+  implicit val errorsR: SnakePickle.Reader[Errors] = SnakePickle.macroR[Errors]
+}
+
+/** @param code
+  *   An error code identifying the error type.
+  * @param message
+  *   A human-readable message providing more details about the error.
+  * @param param
+  *   The name of the parameter that caused the error, if applicable.
+  * @param line
+  *   The line number of the input file where the error occurred, if applicable.
+  */
+case class Data(
+    code: String,
+    message: String,
+    param: Option[String] = None,
+    line: Option[Int] = None
+)
+
+object Data {
+  implicit val dataR: SnakePickle.Reader[Data] = SnakePickle.macroR[Data]
+}
+
+/** @param total
+  *   Total number of requests in the batch.
+  * @param completed
+  *   Number of requests that have been completed successfully.
+  * @param failed
+  *   Number of requests that have failed.
+  */
+case class RequestCounts(
+    total: Int,
+    completed: Int,
+    failed: Int
+)
+
+object RequestCounts {
+  implicit val requestCountsR: SnakePickle.Reader[RequestCounts] = SnakePickle.macroR[RequestCounts]
+}
+
+case class ListBatchResponse(
+    `object`: String = "list",
+    data: Seq[BatchResponse],
+    firstId: String,
+    lastId: String,
+    hasMore: Boolean
+)
+
+object ListBatchResponse {
+  implicit val listBatchResponseR: SnakePickle.Reader[ListBatchResponse] = SnakePickle.macroR[ListBatchResponse]
+}
diff --git a/core/src/main/scala/sttp/openai/requests/batch/QueryParameters.scala b/core/src/main/scala/sttp/openai/requests/batch/QueryParameters.scala
new file mode 100644
index 0000000..d9314e8
--- /dev/null
+++ b/core/src/main/scala/sttp/openai/requests/batch/QueryParameters.scala
@@ -0,0 +1,24 @@
+package sttp.openai.requests.batch
+
+/** @param after
+  *   A cursor for use in pagination. after is an object ID that defines your place in the list. For instance, if you make a list request
+  *   and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the
+  *   list.
+  * @param limit
+  *   A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20.
+  */
+case class QueryParameters(
+    after: Option[String] = None,
+    limit: Option[Int] = None
+) {
+
+  def toMap: Map[String, String] = {
+    val queryParams = after.map("after" -> _) ++
+      limit.map(_.toString).map("limit" -> _)
+    queryParams.toMap
+  }
+}
+
+object QueryParameters {
+  val empty: QueryParameters = QueryParameters(None, None)
+}
diff --git a/core/src/main/scala/sttp/openai/requests/completions/Usage.scala b/core/src/main/scala/sttp/openai/requests/completions/Usage.scala
index 3764ba4..0343261 100644
--- a/core/src/main/scala/sttp/openai/requests/completions/Usage.scala
+++ b/core/src/main/scala/sttp/openai/requests/completions/Usage.scala
@@ -2,8 +2,60 @@ package sttp.openai.requests.completions
 
 import sttp.openai.json.SnakePickle
 
-case class Usage(promptTokens: Int, completionTokens: Int, totalTokens: Int)
+/** @param promptTokens
+  *   Number of tokens in the prompt.
+  * @param completionTokens
+  *   Number of tokens in the generated completion.
+  * @param totalTokens
+  *   Total number of tokens used in the request (prompt + completion).
+  * @param completionTokensDetails
+  *   Breakdown of tokens used in a completion.
+  * @param promptTokensDetails
+  *   Breakdown of tokens used in the prompt.
+  */
+case class Usage(
+    promptTokens: Int,
+    completionTokens: Int,
+    totalTokens: Int,
+    completionTokensDetails: CompletionTokensDetails,
+    promptTokensDetails: PromptTokensDetails
+)
 
 object Usage {
   implicit val choicesR: SnakePickle.Reader[Usage] = SnakePickle.macroR[Usage]
 }
+
+/** @param acceptedPredictionTokens
+  *   When using Predicted Outputs, the number of tokens in the prediction that appeared in the completion.
+  * @param audioTokens
+  *   Audio input tokens generated by the model.
+  * @param reasoningTokens
+  *   Tokens generated by the model for reasoning.
+  * @param rejectedPredictionTokens
+  *   When using Predicted Outputs, the number of tokens in the prediction that did not appear in the completion. However, like reasoning
+  *   tokens, these tokens are still counted in the total completion tokens for purposes of billing, output, and context window limits.
+  */
+case class CompletionTokensDetails(
+    acceptedPredictionTokens: Int,
+    audioTokens: Int,
+    reasoningTokens: Int,
+    rejectedPredictionTokens: Int
+)
+
+object CompletionTokensDetails {
+  implicit val completionTokensDetailsR: SnakePickle.Reader[CompletionTokensDetails] = SnakePickle.macroR[CompletionTokensDetails]
+}
+
+/** @param audioTokens
+  *   Audio input tokens present in the prompt.
+  * @param cachedTokens
+  *   Cached tokens present in the prompt.
+  */
+case class PromptTokensDetails(
+    audioTokens: Int,
+    cachedTokens: Int
+)
+
+object PromptTokensDetails {
+  implicit val promptTokensDetailsR: SnakePickle.Reader[PromptTokensDetails] = SnakePickle.macroR[PromptTokensDetails]
+}
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 1306acb..7b4a29e 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
@@ -191,6 +191,46 @@ object ChatRequestBody {
     *   Controls which (if any) function is called by the model.
     * @param user
     *   A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.
+    * @param store
+    *   Whether or not to store the output of this chat completion request for use in our model distillation or evals products.
+    * @param reasoningEffort
+    *   Constrains effort on reasoning for reasoning models. Currently supported values are low, medium, and high. Reducing reasoning effort
+    *   can result in faster responses and fewer tokens used on reasoning in a response.
+    * @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.
+    * @param logprobs
+    *   Whether to return log probabilities of the output tokens or not. If true, returns the log probabilities of each output token
+    *   returned in the content of message.
+    * @param topLogprobs
+    *   An integer between 0 and 20 specifying the number of most likely tokens to return at each token position, each with an associated
+    *   log probability. logprobs must be set to true if this parameter is used.
+    * @param maxCompletionTokens
+    *   An upper bound for the number of tokens that can be generated for a completion, including visible output tokens and reasoning
+    *   tokens.
+    * @param modalities
+    *   Output types that you would like the model to generate for this request. Most models are capable of generating text, which is the
+    *   default: ["text"]. The gpt-4o-audio-preview model can also be used to generate audio. To request that this model generate both text
+    *   and audio responses, you can use: ["text", "audio"].
+    * @param serviceTier
+    *   Specifies the latency tier to use for processing the request. This parameter is relevant for customers subscribed to the scale tier
+    *   service:
+    *   - If set to 'auto', and the Project is Scale tier enabled, the system will utilize scale tier credits until they are exhausted.
+    *   - If set to 'auto', and the Project is not Scale tier enabled, the request will be processed using the default service tier with a
+    *     lower uptime SLA and no latency guarantee.
+    *   - If set to 'default', the request will be processed using the default service tier with a lower uptime SLA and no latency
+    *     guarantee.
+    *   - When not set, the default behavior is 'auto'.
+    * @param parallelToolCalls
+    *   Whether to enable parallel function calling during tool use.
+    * @param streamOptions
+    *   Options for streaming response. Only set this when you set stream: true.
+    * @param prediction
+    *   Configuration for a Predicted Output, which can greatly improve response times when large parts of the model response are known
+    *   ahead of time. This is most common when you are regenerating a file with only minor changes to most of the content.
+    * @param audio
+    *   Parameters for audio output. Required when audio output is requested with modalities: ["audio"].
     */
   case class ChatBody(
       messages: Seq[Message],
@@ -207,7 +247,19 @@ object ChatRequestBody {
       topP: Option[Double] = None,
       tools: Option[Seq[Tool]] = None,
       toolChoice: Option[ToolChoice] = None,
-      user: Option[String] = None
+      user: Option[String] = None,
+      store: Option[Boolean] = None,
+      reasoningEffort: Option[ReasoningEffort] = None,
+      metadata: Option[Map[String, String]] = None,
+      logprobs: Option[Boolean] = None,
+      topLogprobs: Option[Int] = None,
+      maxCompletionTokens: Option[Int] = None,
+      modalities: Option[Seq[String]] = None,
+      serviceTier: Option[String] = None,
+      parallelToolCalls: Option[Boolean] = None,
+      streamOptions: Option[StreamOptions] = None,
+      prediction: Option[Prediction] = None,
+      audio: Option[Audio] = None
   )
 
   object ChatBody {
@@ -220,6 +272,128 @@ object ChatRequestBody {
     implicit val chatRequestW: SnakePickle.Writer[ChatBody] = SnakePickle.macroW[ChatBody]
   }
 
+  /** @param voice
+    *   The voice the model uses to respond. Supported voices are ash, ballad, coral, sage, and verse (also supported but not recommended
+    *   are alloy, echo, and shimmer; these voices are less expressive).
+    * @param format
+    *   Specifies the output audio format. Must be one of wav, mp3, flac, opus, or pcm16.
+    */
+  case class Audio(
+      voice: Voice,
+      format: Format
+  )
+
+  object Audio {
+    implicit val audioW: SnakePickle.Writer[Audio] = SnakePickle.macroW[Audio]
+  }
+
+  sealed abstract class Voice(val value: String)
+
+  object Voice {
+    case object Ash extends Voice("ash")
+    case object Ballad extends Voice("ballad")
+    case object Coral extends Voice("coral")
+    case object Sage extends Voice("sage")
+    case object Verse extends Voice("verse")
+    case object Alloy extends Voice("alloy")
+    case object Echo extends Voice("echo")
+    case object Shimmer extends Voice("shimmer")
+    case class CustomVoice(customVoice: String) extends Voice(customVoice)
+
+    implicit val voiceW: SnakePickle.Writer[Voice] = SnakePickle
+      .writer[ujson.Value]
+      .comap[Voice](voice => SnakePickle.writeJs(voice.value))
+  }
+
+  sealed abstract class Format(val value: String)
+
+  object Format {
+    case object Wav extends Format("wav")
+    case object Mp3 extends Format("mp3")
+    case object Flac extends Format("flac")
+    case object Opus extends Format("opus")
+    case object Pcm16 extends Format("pcm16")
+    case class CustomFormat(customFormat: String) extends Format(customFormat)
+
+    implicit val formatW: SnakePickle.Writer[Format] = SnakePickle
+      .writer[ujson.Value]
+      .comap[Format](format => SnakePickle.writeJs(format.value))
+  }
+
+  /** @param `type`
+    *   The type of the predicted content you want to provide. This type is currently always content.
+    * @param content
+    *   The content that should be matched when generating a model response. If generated tokens would match this content, the entire model
+    *   response can be returned much more quickly.
+    */
+  case class Prediction(
+      `type`: String,
+      content: Content
+  )
+
+  object Prediction {
+    implicit val predictionW: SnakePickle.Writer[Prediction] = SnakePickle.macroW[Prediction]
+  }
+
+  sealed trait Content
+  case class SingleContent(value: String) extends Content
+  case class MultipartContent(value: Seq[ContentPart]) extends Content
+
+  object Content {
+    implicit val contentW: SnakePickle.Writer[Content] = SnakePickle
+      .writer[ujson.Value]
+      .comap[Content] {
+        case SingleContent(value)    => SnakePickle.writeJs(value)
+        case MultipartContent(value) => SnakePickle.writeJs(value)
+      }
+  }
+
+  /** An array of content parts with a defined type. Supported options differ based on the model being used to generate the response. Can
+    * contain text inputs.
+    *
+    * @param `type`
+    *   The type of the content part.
+    * @param text
+    *   The text content.
+    */
+  case class ContentPart(
+      `type`: String,
+      text: String
+  )
+
+  object ContentPart {
+    implicit val contentPartW: SnakePickle.Writer[ContentPart] = SnakePickle.macroW[ContentPart]
+  }
+
+  /** @param includeUsage
+    *   If set, an additional chunk will be streamed before the data: [DONE] message. The usage field on this chunk shows the token usage
+    *   statistics for the entire request, and the choices field will always be an empty array. All other chunks will also include a usage
+    *   field, but with a null value.
+    */
+  case class StreamOptions(includeUsage: Option[Boolean] = None)
+
+  object StreamOptions {
+    implicit val streamOptionsW: SnakePickle.Writer[StreamOptions] = SnakePickle.macroW[StreamOptions]
+  }
+
+  sealed abstract class ReasoningEffort(val value: String)
+
+  object ReasoningEffort {
+
+    implicit val reasoningEffortW: SnakePickle.Writer[ReasoningEffort] = SnakePickle
+      .writer[ujson.Value]
+      .comap[ReasoningEffort](reasoningEffort => SnakePickle.writeJs(reasoningEffort.value))
+
+    case object Low extends ReasoningEffort("low")
+
+    case object Medium extends ReasoningEffort("medium")
+
+    case object High extends ReasoningEffort("high")
+
+    case class CustomReasoningEffort(customReasoningEffort: String) extends ReasoningEffort(customReasoningEffort)
+
+  }
+
   sealed abstract class ChatCompletionModel(val value: String)
 
   object ChatCompletionModel {
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 d95cd22..1b52546 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
@@ -18,16 +18,108 @@ object ChatRequestResponseData {
     implicit val messageRW: SnakePickle.Reader[Message] = SnakePickle.macroR[Message]
   }
 
+  /** Represents a choice in the chat completion.
+    *
+    * @param message
+    *   The message associated with this choice.
+    * @param finishReason
+    *   The reason the model stopped generating tokens. This will be stop if the model hit a natural stop point or a provided stop sequence,
+    *   length if the maximum number of tokens specified in the request was reached, content_filter if content was omitted due to a flag
+    *   from our content filters, tool_calls if the model called a tool, or function_call (deprecated) if the model called a function.
+    * @param index
+    *   The index of this choice.
+    * @param logprobs
+    *   Log probability information for the choice.
+    */
   case class Choices(
       message: Message,
       finishReason: String,
-      index: Int
+      index: Int,
+      logprobs: Option[Logprobs] = None
   )
 
   object Choices {
     implicit val choicesR: SnakePickle.Reader[Choices] = SnakePickle.macroR[Choices]
   }
 
+  /** @param content
+    *   A list of message content tokens with log probability information.
+    * @param refusal
+    *   A list of message refusal tokens with log probability information.
+    */
+  case class Logprobs(
+      content: Option[Seq[LogprobData]] = None,
+      refusal: Option[Seq[LogprobData]] = None
+  )
+
+  object Logprobs {
+    implicit val logprobsR: SnakePickle.Reader[Logprobs] = SnakePickle.macroR[Logprobs]
+  }
+
+  /** @param token
+    *   The token.
+    * @param logprob
+    *   The log probability of this token, if it is within the top 20 most likely tokens. Otherwise, the value -9999.0 is used to signify
+    *   that the token is very unlikely.
+    * @param bytes
+    *   A list of integers representing the UTF-8 bytes representation of the token. Useful in instances where characters are represented by
+    *   multiple tokens and their byte representations must be combined to generate the correct text representation. Can be null if there is
+    *   no bytes representation for the token.
+    * @param topLogprobs
+    *   List of the most likely tokens and their log probability, at this token position. In rare cases, there may be fewer than the number
+    *   of requested top_logprobs returned.
+    */
+  case class LogprobData(
+      token: String,
+      logprob: Float,
+      bytes: Option[Seq[Int]] = None,
+      topLogprobs: Seq[TopLogprobs]
+  )
+
+  object LogprobData {
+    implicit val contentR: SnakePickle.Reader[LogprobData] = SnakePickle.macroR[LogprobData]
+  }
+
+  /** @param token
+    *   The token.
+    * @param logprob
+    *   The log probability of this token, if it is within the top 20 most likely tokens. Otherwise, the value -9999.0 is used to signify
+    *   that the token is very unlikely.
+    * @param bytes
+    *   A list of integers representing the UTF-8 bytes representation of the token. Useful in instances where characters are represented by
+    *   multiple tokens and their byte representations must be combined to generate the correct text representation. Can be null if there is
+    *   no bytes representation for the token.
+    */
+  case class TopLogprobs(
+      token: String,
+      logprob: Float,
+      bytes: Option[Seq[Int]] = None
+  )
+
+  object TopLogprobs {
+    implicit val topLogprobsR: SnakePickle.Reader[TopLogprobs] = SnakePickle.macroR[TopLogprobs]
+  }
+
+  /** Represents the response of a chat completion.
+    *
+    * @param id
+    *   A unique identifier for the chat completion.
+    * @param choices
+    *   A list of chat completion choices. Can be more than one if n is greater than 1.
+    * @param created
+    *   The Unix timestamp (in seconds) of when the chat completion was created.
+    * @param model
+    *   The model used for the chat completion.
+    * @param `object`
+    *   The object type, which is always chat.completion.
+    * @param usage
+    *   Usage statistics for the completion request.
+    * @param systemFingerprint
+    *   This fingerprint represents the backend configuration that the model runs with. Can be used in conjunction with the seed request
+    *   parameter to understand when backend changes have been made that might impact determinism.
+    * @param serviceTier
+    *   The service tier used for processing the request.
+    */
   case class ChatResponse(
       id: String,
       choices: Seq[Choices],
@@ -35,7 +127,8 @@ object ChatRequestResponseData {
       model: String,
       `object`: String,
       usage: Usage,
-      systemFingerprint: Option[String] = None
+      systemFingerprint: Option[String] = None,
+      serviceTier: Option[String] = None
   )
 
   object ChatResponse {
diff --git a/core/src/main/scala/sttp/openai/requests/embeddings/EmbeddingsRequestBody.scala b/core/src/main/scala/sttp/openai/requests/embeddings/EmbeddingsRequestBody.scala
index 760ff07..59635b0 100644
--- a/core/src/main/scala/sttp/openai/requests/embeddings/EmbeddingsRequestBody.scala
+++ b/core/src/main/scala/sttp/openai/requests/embeddings/EmbeddingsRequestBody.scala
@@ -12,8 +12,10 @@ object EmbeddingsRequestBody {
     *   Input text to get embeddings for, encoded as a string or array of tokens.
     * @param user
     *   A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.
+    * @param dimensions
+    *   The number of dimensions for the embeddings. Only supported in text-embedding-3 and later models.
     */
-  case class EmbeddingsBody(model: EmbeddingsModel, input: EmbeddingsInput, user: Option[String] = None)
+  case class EmbeddingsBody(model: EmbeddingsModel, input: EmbeddingsInput, user: Option[String] = None, dimensions: Option[Int] = None)
 
   object EmbeddingsBody {
     implicit val embeddingsBodyWriter: SnakePickle.Writer[EmbeddingsBody] = SnakePickle.macroW
diff --git a/core/src/main/scala/sttp/openai/requests/finetuning/QueryParameters.scala b/core/src/main/scala/sttp/openai/requests/finetuning/QueryParameters.scala
index 891de51..d3683e2 100644
--- a/core/src/main/scala/sttp/openai/requests/finetuning/QueryParameters.scala
+++ b/core/src/main/scala/sttp/openai/requests/finetuning/QueryParameters.scala
@@ -1,11 +1,9 @@
 package sttp.openai.requests.finetuning
 
 /** @param after
-  *   A cursor for use in pagination. after is an object ID that defines your place in the list. For instance, if you make a list request
-  *   and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the
-  *   list
+  *   Identifier for the last job from the previous pagination request.
   * @param limit
-  *   Defaults to 20 A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 20.
+  *   Number of fine-tuning jobs to retrieve.
   */
 case class QueryParameters(
     after: Option[String] = None,
@@ -14,7 +12,7 @@ case class QueryParameters(
 
   def toMap: Map[String, String] = {
     val queryParams = after.map("after" -> _) ++
-      limit.map(_.toString).map("order" -> _)
+      limit.map(_.toString).map("limit" -> _)
     queryParams.toMap
   }
 }
diff --git a/core/src/main/scala/sttp/openai/requests/threads/messages/ThreadMessagesResponseData.scala b/core/src/main/scala/sttp/openai/requests/threads/messages/ThreadMessagesResponseData.scala
index 98010ac..7f17797 100644
--- a/core/src/main/scala/sttp/openai/requests/threads/messages/ThreadMessagesResponseData.scala
+++ b/core/src/main/scala/sttp/openai/requests/threads/messages/ThreadMessagesResponseData.scala
@@ -65,6 +65,14 @@ object ThreadMessagesResponseData {
   object ListMessagesResponse {
     implicit val listMessagesResponseR: SnakePickle.Reader[ListMessagesResponse] = SnakePickle.macroR[ListMessagesResponse]
   }
+  case class DeleteMessageResponse(
+      `object`: String = "thread.message.deleted",
+      id: String,
+      deleted: Boolean
+  )
+  object DeleteMessageResponse {
+    implicit val deleteMessageResponseR: SnakePickle.Reader[DeleteMessageResponse] = SnakePickle.macroR[DeleteMessageResponse]
+  }
 
   sealed trait Annotation
 
diff --git a/core/src/main/scala/sttp/openai/requests/threads/runs/ThreadRunsResponseData.scala b/core/src/main/scala/sttp/openai/requests/threads/runs/ThreadRunsResponseData.scala
index da82999..11009d8 100644
--- a/core/src/main/scala/sttp/openai/requests/threads/runs/ThreadRunsResponseData.scala
+++ b/core/src/main/scala/sttp/openai/requests/threads/runs/ThreadRunsResponseData.scala
@@ -1,7 +1,6 @@
 package sttp.openai.requests.threads.runs
 
 import sttp.openai.json.SnakePickle
-import sttp.openai.requests.completions.Usage
 import sttp.openai.requests.completions.chat.message.{Tool, ToolResources}
 
 object ThreadRunsResponseData {
@@ -442,4 +441,17 @@ object ThreadRunsResponseData {
   object ListRunStepsResponse {
     implicit val listRunStepsResponseR: SnakePickle.Reader[ListRunStepsResponse] = SnakePickle.macroR[ListRunStepsResponse]
   }
+
+  /** @param promptTokens
+    *   Number of tokens in the prompt.
+    * @param completionTokens
+    *   Number of tokens in the generated completion.
+    * @param totalTokens
+    *   Total number of tokens used in the request (prompt + completion).
+    */
+  case class Usage(promptTokens: Int, completionTokens: Int, totalTokens: Int)
+
+  object Usage {
+    implicit val choicesR: SnakePickle.Reader[Usage] = SnakePickle.macroR[Usage]
+  }
 }


Test patch:
diff --git a/core/src/test/scala/sttp/openai/fixtures/AssistantsFixture.scala b/core/src/test/scala/sttp/openai/fixtures/AssistantsFixture.scala
index cbfc97b..75a25fe 100644
--- a/core/src/test/scala/sttp/openai/fixtures/AssistantsFixture.scala
+++ b/core/src/test/scala/sttp/openai/fixtures/AssistantsFixture.scala
@@ -7,7 +7,10 @@ object AssistantsFixture {
       |  "instructions": "You are a personal math tutor. When asked a question, write and run Python code to answer the question.",
       |  "name": "Math Tutor",
       |  "tools": [{"type": "code_interpreter"}],
-      |  "model": "gpt-4"
+      |  "model": "gpt-4",
+      |  "reasoning_effort": "low",
+      |  "temperature": 1.0,
+      |  "top_p": 1.0
       |}""".stripMargin
 
   val jsonCreateAssistantResponse: String =
@@ -113,7 +116,10 @@ object AssistantsFixture {
       |     "file_search": {
       |        "vector_store_ids": ["vs_1", "vs_3"]
       |    }
-      |  }
+      |  },
+      |  "reasoning_effort": "low",
+      |  "temperature": 1.0,
+      |  "top_p": 1.0
       |}
       |""".stripMargin
 
diff --git a/core/src/test/scala/sttp/openai/fixtures/BatchFixture.scala b/core/src/test/scala/sttp/openai/fixtures/BatchFixture.scala
new file mode 100644
index 0000000..c220078
--- /dev/null
+++ b/core/src/test/scala/sttp/openai/fixtures/BatchFixture.scala
@@ -0,0 +1,77 @@
+package sttp.openai.fixtures
+
+import sttp.openai.requests.batch.{BatchResponse, RequestCounts}
+
+object BatchFixture {
+
+  val jsonCreateBatchRequest: String = """{
+                                 |  "input_file_id": "file-id",
+                                 |  "endpoint": "/v1/chat/completions",
+                                 |  "completion_window": "24h",
+                                 |  "metadata": {
+                                 |    "key1": "value1",
+                                 |    "key2": "value2"
+                                 |  }
+                                 |}""".stripMargin
+
+  val jsonCreateBatchResponse: String = """{
+                                  |  "id": "batch_abc123",
+                                  |  "object": "batch",
+                                  |  "endpoint": "/v1/completions",
+                                  |  "errors": null,
+                                  |  "input_file_id": "file-abc123",
+                                  |  "completion_window": "24h",
+                                  |  "status": "completed",
+                                  |  "output_file_id": "file-cvaTdG",
+                                  |  "error_file_id": "file-HOWS94",
+                                  |  "created_at": 1711471533,
+                                  |  "in_progress_at": 1711471538,
+                                  |  "expires_at": 1711557933,
+                                  |  "finalizing_at": 1711493133,
+                                  |  "completed_at": 1711493163,
+                                  |  "failed_at": null,
+                                  |  "expired_at": null,
+                                  |  "cancelling_at": null,
+                                  |  "cancelled_at": null,
+                                  |  "request_counts": {
+                                  |    "total": 100,
+                                  |    "completed": 95,
+                                  |    "failed": 5
+                                  |  },
+                                  |  "metadata": {
+                                  |    "customer_id": "user_123456789",
+                                  |    "batch_description": "Nightly eval job"
+                                  |  }
+                                  |}""".stripMargin
+
+  val jsonListBatchResponse: String = s"""{
+                                   |  "object": "list",
+                                   |  "data": [$jsonCreateBatchResponse],
+                                   |  "first_id": "ftckpt_zc4Q7MP6XxulcVzj4MZdwsAB",
+                                   |  "last_id": "ftckpt_enQCFmOTGj3syEpYVhBRLTSy",
+                                   |  "has_more": true
+                                   |}""".stripMargin
+
+  val batchResponse: BatchResponse = BatchResponse(
+    id = "batch_abc123",
+    endpoint = "/v1/completions",
+    errors = None,
+    inputFileId = "file-abc123",
+    completionWindow = "24h",
+    status = "completed",
+    outputFileId = Some("file-cvaTdG"),
+    errorFileId = Some("file-HOWS94"),
+    createdAt = 1711471533,
+    inProgressAt = Some(1711471538),
+    expiresAt = Some(1711557933),
+    finalizingAt = Some(1711493133),
+    completedAt = Some(1711493163),
+    failedAt = None,
+    expiredAt = None,
+    cancellingAt = None,
+    cancelledAt = None,
+    requestCounts = Some(RequestCounts(total = 100, completed = 95, failed = 5)),
+    metadata = Some(Map("customer_id" -> "user_123456789", "batch_description" -> "Nightly eval job"))
+  )
+
+}
diff --git a/core/src/test/scala/sttp/openai/fixtures/ChatFixture.scala b/core/src/test/scala/sttp/openai/fixtures/ChatFixture.scala
index 25e5da9..f52d345 100644
--- a/core/src/test/scala/sttp/openai/fixtures/ChatFixture.scala
+++ b/core/src/test/scala/sttp/openai/fixtures/ChatFixture.scala
@@ -111,7 +111,34 @@ object ChatFixture {
       |      "name": "function"
       |    }
       |  },
-      |  "user": "testUser"
+      |  "user": "testUser",
+      |  "store": true,
+      |  "reasoning_effort": "low",
+      |  "metadata": {
+      |    "key": "value"
+      |  },
+      |  "logprobs": true,
+      |  "top_logprobs": 1,
+      |  "max_completion_tokens": 10,
+      |  "modalities": ["text", "audio"],
+      |  "service_tier": "advanced",
+      |  "parallel_tool_calls": true,
+      |  "stream_options": {
+      |    "include_usage": true
+      |  },
+      |  "prediction": {
+      |    "type": "content",
+      |    "content": [
+      |      {
+      |        "type": "code",
+      |        "text": "simple text"
+      |      }
+      |    ]
+      |  },
+      |  "audio": {
+      |    "voice": "ash",
+      |    "format": "mp3"
+      |  }
       |}""".stripMargin
 
   val jsonResponse: String =
@@ -124,7 +151,17 @@ object ChatFixture {
       |  "usage": {
       |    "prompt_tokens": 10,
       |    "completion_tokens": 10,
-      |    "total_tokens": 20
+      |    "total_tokens": 20,
+      |    "prompt_tokens_details": {
+      |      "cached_tokens": 1,
+      |      "audio_tokens": 2
+      |    },
+      |    "completion_tokens_details": {
+      |      "reasoning_tokens": 4,
+      |      "accepted_prediction_tokens": 3,
+      |      "rejected_prediction_tokens": 2,
+      |      "audio_tokens": 1
+      |    }
       |  },
       |  "choices": [
       |    {
@@ -149,9 +186,40 @@ object ChatFixture {
       |        ]
       |      },
       |      "finish_reason": "stop",
-      |      "index": 0
+      |      "index": 0,
+      |      "logprobs": {
+      |        "content": [
+      |          {
+      |            "token": "Hello",
+      |            "logprob": -0.1,
+      |            "bytes": [2, 3, 4],
+      |            "top_logprobs": [
+      |              {
+      |                "token": "Hello",
+      |                "logprob": -0.2,
+      |                "bytes": [4, 5, 6]
+      |              }
+      |            ]
+      |          }
+      |        ],
+      |        "refusal": [
+      |          {
+      |            "token": "Hello",
+      |            "logprob": -0.1,
+      |            "bytes": [2, 3, 4],
+      |            "top_logprobs": [
+      |              {
+      |                "token": "Hello",
+      |                "logprob": -0.2,
+      |                "bytes": [4, 5, 6]
+      |              }
+      |            ]
+      |          }
+      |        ]
+      |      }
       |    }
-      |  ]
+      |  ],
+      |  "service_tier": "advanced"
       |}
       |""".stripMargin
 
diff --git a/core/src/test/scala/sttp/openai/fixtures/CompletionsFixture.scala b/core/src/test/scala/sttp/openai/fixtures/CompletionsFixture.scala
index 18e0285..3720625 100644
--- a/core/src/test/scala/sttp/openai/fixtures/CompletionsFixture.scala
+++ b/core/src/test/scala/sttp/openai/fixtures/CompletionsFixture.scala
@@ -17,7 +17,17 @@ object CompletionsFixture {
                      |  "usage": {
                      |    "prompt_tokens": 5,
                      |    "completion_tokens": 8,
-                     |    "total_tokens": 13
+                     |    "total_tokens": 13,
+                     |    "prompt_tokens_details": {
+                     |      "cached_tokens": 1,
+                     |      "audio_tokens": 2
+                     |    },
+                     |    "completion_tokens_details": {
+                     |      "reasoning_tokens": 4,
+                     |      "accepted_prediction_tokens": 3,
+                     |      "rejected_prediction_tokens": 2,
+                     |      "audio_tokens": 1
+                     |    }
                      |  }
                      |}""".stripMargin
 
@@ -43,7 +53,17 @@ object CompletionsFixture {
                     |  "usage":{
                     |    "prompt_tokens":11,
                     |    "completion_tokens":14,
-                    |    "total_tokens":25
+                    |    "total_tokens":25,
+                    |    "prompt_tokens_details": {
+                    |      "cached_tokens": 1,
+                    |      "audio_tokens": 2
+                    |    },
+                    |    "completion_tokens_details": {
+                    |      "reasoning_tokens": 4,
+                    |      "accepted_prediction_tokens": 3,
+                    |      "rejected_prediction_tokens": 2,
+                    |      "audio_tokens": 1
+                    |    }
                     |  }
                     |}
                     |
diff --git a/core/src/test/scala/sttp/openai/fixtures/ThreadMessagesFixture.scala b/core/src/test/scala/sttp/openai/fixtures/ThreadMessagesFixture.scala
index f318907..5404dd1 100644
--- a/core/src/test/scala/sttp/openai/fixtures/ThreadMessagesFixture.scala
+++ b/core/src/test/scala/sttp/openai/fixtures/ThreadMessagesFixture.scala
@@ -138,4 +138,11 @@ object ThreadMessagesFixture {
       |  }
       |}
       |""".stripMargin
+
+  val jsonDeleteMessageResponse: String =
+    """{
+       |  "id": "msg_abc123",
+       |  "object": "thread.message.deleted",
+       |  "deleted": true
+       |}""".stripMargin
 }
diff --git a/core/src/test/scala/sttp/openai/requests/BatchDataSpec.scala b/core/src/test/scala/sttp/openai/requests/BatchDataSpec.scala
new file mode 100644
index 0000000..2ce3aec
--- /dev/null
+++ b/core/src/test/scala/sttp/openai/requests/BatchDataSpec.scala
@@ -0,0 +1,55 @@
+package sttp.openai.requests
+
+import org.scalatest.EitherValues
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+import sttp.openai.fixtures.BatchFixture
+import sttp.openai.json.{SnakePickle, SttpUpickleApiExtension}
+import sttp.openai.requests.batch.{BatchRequestBody, BatchResponse, ListBatchResponse}
+
+class BatchDataSpec extends AnyFlatSpec with Matchers with EitherValues {
+
+  "Given create batch request as case class" should "be properly serialized to Json" in {
+    // given
+    val givenRequest = BatchRequestBody(
+      inputFileId = "file-id",
+      endpoint = "/v1/chat/completions",
+      completionWindow = "24h",
+      metadata = Some(Map("key1" -> "value1", "key2" -> "value2"))
+    )
+    val jsonRequest: ujson.Value = ujson.read(BatchFixture.jsonCreateBatchRequest)
+    // when
+    val serializedJson: ujson.Value = SnakePickle.writeJs(givenRequest)
+    // then
+    serializedJson shouldBe jsonRequest
+  }
+
+  "Given create batch response as Json" should "be properly deserialized to case class" in {
+    // given
+    val jsonResponse = BatchFixture.jsonCreateBatchResponse
+    val expectedResponse: BatchResponse = BatchFixture.batchResponse
+    // when
+    val deserializedJsonResponse: Either[Exception, BatchResponse] =
+      SttpUpickleApiExtension.deserializeJsonSnake[BatchResponse].apply(jsonResponse)
+    // then
+    deserializedJsonResponse.value shouldBe expectedResponse
+  }
+
+  "Given list batch response as Json" should "be properly deserialized to case class" in {
+    // given
+    val jsonResponse = BatchFixture.jsonListBatchResponse
+    val expectedResponse: ListBatchResponse = ListBatchResponse(
+      data = Seq(BatchFixture.batchResponse),
+      hasMore = true,
+      firstId = "ftckpt_zc4Q7MP6XxulcVzj4MZdwsAB",
+      lastId = "ftckpt_enQCFmOTGj3syEpYVhBRLTSy"
+    )
+    // when
+    val deserializedJsonResponse: Either[Exception, ListBatchResponse] =
+      SttpUpickleApiExtension.deserializeJsonSnake[ListBatchResponse].apply(jsonResponse)
+
+    // then
+    deserializedJsonResponse.value shouldBe expectedResponse
+  }
+
+}
diff --git a/core/src/test/scala/sttp/openai/requests/assistants/AssistantsDataSpec.scala b/core/src/test/scala/sttp/openai/requests/assistants/AssistantsDataSpec.scala
index 07a253e..32b9552 100644
--- a/core/src/test/scala/sttp/openai/requests/assistants/AssistantsDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/assistants/AssistantsDataSpec.scala
@@ -20,7 +20,10 @@ class AssistantsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
       instructions = Some("You are a personal math tutor. When asked a question, write and run Python code to answer the question."),
       name = Some("Math Tutor"),
       tools = Seq(CodeInterpreterTool),
-      model = "gpt-4"
+      model = "gpt-4",
+      reasoningEffort = Some(ReasoningEffort.Low),
+      temperature = Some(1.0f),
+      topP = Some(1.0f)
     )
 
     val jsonRequest: ujson.Value = ujson.read(fixtures.AssistantsFixture.jsonCreateAssistantRequest)
@@ -213,7 +216,10 @@ class AssistantsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
       ),
       tools = Seq(FileSearchTool),
       model = Some("gpt-4"),
-      toolResources = Some(ToolResources(None, Some(FileSearchToolResource(Some(Seq("vs_1", "vs_3"))))))
+      toolResources = Some(ToolResources(None, Some(FileSearchToolResource(Some(Seq("vs_1", "vs_3")))))),
+      reasoningEffort = Some(ReasoningEffort.Low),
+      temperature = Some(1.0f),
+      topP = Some(1.0f)
     )
 
     val jsonRequest: ujson.Value = ujson.read(fixtures.AssistantsFixture.jsonModifyAssistantRequest)
diff --git a/core/src/test/scala/sttp/openai/requests/completions/CompletionsDataSpec.scala b/core/src/test/scala/sttp/openai/requests/completions/CompletionsDataSpec.scala
index 2901fb9..0601a9a 100644
--- a/core/src/test/scala/sttp/openai/requests/completions/CompletionsDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/completions/CompletionsDataSpec.scala
@@ -31,7 +31,14 @@ class CompletionsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
       usage = Usage(
         promptTokens = 5,
         completionTokens = 8,
-        totalTokens = 13
+        totalTokens = 13,
+        completionTokensDetails = CompletionTokensDetails(
+          acceptedPredictionTokens = 3,
+          audioTokens = 1,
+          reasoningTokens = 4,
+          rejectedPredictionTokens = 2
+        ),
+        promptTokensDetails = PromptTokensDetails(audioTokens = 2, cachedTokens = 1)
       )
     )
 
@@ -97,7 +104,14 @@ class CompletionsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
       usage = Usage(
         promptTokens = 11,
         completionTokens = 14,
-        totalTokens = 25
+        totalTokens = 25,
+        completionTokensDetails = CompletionTokensDetails(
+          acceptedPredictionTokens = 3,
+          audioTokens = 1,
+          reasoningTokens = 4,
+          rejectedPredictionTokens = 2
+        ),
+        promptTokensDetails = PromptTokensDetails(audioTokens = 2, cachedTokens = 1)
       )
     )
 
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 04129b8..f0950b2 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
@@ -4,17 +4,18 @@ import org.scalatest.EitherValues
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 import sttp.openai.fixtures
-import sttp.openai.json.SnakePickle
-import sttp.openai.json.SttpUpickleApiExtension
+import sttp.openai.json.{SnakePickle, SttpUpickleApiExtension}
 import sttp.openai.requests.completions.Stop.SingleStop
-import sttp.openai.requests.completions.Usage
+import sttp.openai.requests.completions.chat.ChatRequestBody.Format.Mp3
+import sttp.openai.requests.completions.chat.ChatRequestBody.Voice.Ash
+import sttp.openai.requests.completions.{CompletionTokensDetails, PromptTokensDetails, Usage}
 import sttp.openai.utils.ChatCompletionFixtures._
 
 class ChatDataSpec extends AnyFlatSpec with Matchers with EitherValues {
 
   "Given chat completions response as Json" should "be properly deserialized to case class" in {
-    import ChatRequestResponseData._
     import ChatRequestResponseData.ChatResponse._
+    import ChatRequestResponseData._
 
     // given
     val jsonResponse = fixtures.ChatFixture.jsonResponse
@@ -22,7 +23,14 @@ class ChatDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     val usage: Usage = Usage(
       promptTokens = 10,
       completionTokens = 10,
-      totalTokens = 20
+      totalTokens = 20,
+      completionTokensDetails = CompletionTokensDetails(
+        acceptedPredictionTokens = 3,
+        audioTokens = 1,
+        reasoningTokens = 4,
+        rejectedPredictionTokens = 2
+      ),
+      promptTokensDetails = PromptTokensDetails(audioTokens = 2, cachedTokens = 1)
     )
 
     val message: Message = Message(
@@ -34,7 +42,31 @@ class ChatDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     val choices: Choices = Choices(
       message = message,
       finishReason = "stop",
-      index = 0
+      index = 0,
+      logprobs = Some(
+        Logprobs(
+          content = Some(
+            Seq(
+              LogprobData(
+                token = "Hello",
+                logprob = -0.1f,
+                bytes = Some(Seq(2, 3, 4)),
+                topLogprobs = Seq(TopLogprobs(token = "Hello", logprob = -0.2f, bytes = Some(Seq(4, 5, 6))))
+              )
+            )
+          ),
+          refusal = Some(
+            Seq(
+              LogprobData(
+                token = "Hello",
+                logprob = -0.1f,
+                bytes = Some(Seq(2, 3, 4)),
+                topLogprobs = Seq(TopLogprobs(token = "Hello", logprob = -0.2f, bytes = Some(Seq(4, 5, 6))))
+              )
+            )
+          )
+        )
+      )
     )
 
     val expectedResponse: ChatResponse = ChatResponse(
@@ -44,7 +76,8 @@ class ChatDataSpec extends AnyFlatSpec with Matchers with EitherValues {
       model = "gpt-3.5-turbo-0301",
       usage = usage,
       choices = Seq(choices),
-      systemFingerprint = Some("systemFingerprint")
+      systemFingerprint = Some("systemFingerprint"),
+      serviceTier = Some("advanced")
     )
 
     // when
@@ -72,7 +105,20 @@ class ChatDataSpec extends AnyFlatSpec with Matchers with EitherValues {
       responseFormat = Some(ResponseFormat.JsonObject),
       toolChoice = Some(ToolChoice.ToolFunction("function")),
       stop = Some(SingleStop("\n")),
-      user = Some("testUser")
+      user = Some("testUser"),
+      store = Some(true),
+      reasoningEffort = Some(ReasoningEffort.Low),
+      metadata = Some(Map("key" -> "value")),
+      logprobs = Some(true),
+      topLogprobs = Some(1),
+      maxCompletionTokens = Some(10),
+      modalities = Some(Seq("text", "audio")),
+      serviceTier = Some("advanced"),
+      parallelToolCalls = Some(true),
+      streamOptions = Some(StreamOptions(includeUsage = Some(true))),
+      prediction =
+        Some(Prediction(`type` = "content", content = MultipartContent(value = Seq(ContentPart(`type` = "code", text = "simple text"))))),
+      audio = Some(Audio(voice = Ash, format = Mp3))
     )
 
     val jsonRequest: ujson.Value = ujson.read(fixtures.ChatFixture.jsonRequest)
diff --git a/core/src/test/scala/sttp/openai/requests/threads/messages/ThreadMessagesDataSpec.scala b/core/src/test/scala/sttp/openai/requests/threads/messages/ThreadMessagesDataSpec.scala
index 883c2e0..57010a0 100644
--- a/core/src/test/scala/sttp/openai/requests/threads/messages/ThreadMessagesDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/threads/messages/ThreadMessagesDataSpec.scala
@@ -7,7 +7,7 @@ import sttp.openai.fixtures
 import sttp.openai.json.{SnakePickle, SttpUpickleApiExtension}
 import sttp.openai.requests.completions.chat.message.Attachment
 import sttp.openai.requests.threads.messages.ThreadMessagesResponseData.Content.{TextContent, TextContentValue}
-import sttp.openai.requests.threads.messages.ThreadMessagesResponseData.{ListMessagesResponse, MessageData}
+import sttp.openai.requests.threads.messages.ThreadMessagesResponseData.{DeleteMessageResponse, ListMessagesResponse, MessageData}
 
 class ThreadMessagesDataSpec extends AnyFlatSpec with Matchers with EitherValues {
 
@@ -187,4 +187,20 @@ class ThreadMessagesDataSpec extends AnyFlatSpec with Matchers with EitherValues
     // then
     givenResponse.value shouldBe expectedResponse
   }
+
+  "Given delete message response as Json" should "be properly deserialized to case class" in {
+    import DeleteMessageResponse._
+    // given
+    val jsonResponse = fixtures.ThreadMessagesFixture.jsonDeleteMessageResponse
+    val expectedResponse: DeleteMessageResponse = DeleteMessageResponse(
+      id = "msg_abc123",
+      deleted = true
+    )
+
+    // when
+    val givenResponse: Either[Exception, DeleteMessageResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+
+    // then
+    givenResponse.value shouldBe expectedResponse
+  }
 }
diff --git a/core/src/test/scala/sttp/openai/requests/threads/runs/ThreadRunsDataSpec.scala b/core/src/test/scala/sttp/openai/requests/threads/runs/ThreadRunsDataSpec.scala
index 67914f9..cc1f84e 100644
--- a/core/src/test/scala/sttp/openai/requests/threads/runs/ThreadRunsDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/threads/runs/ThreadRunsDataSpec.scala
@@ -6,13 +6,18 @@ import org.scalatest.matchers.should.Matchers
 import sttp.openai.fixtures
 import sttp.openai.json.{SnakePickle, SttpUpickleApiExtension}
 import sttp.openai.requests.completions.chat.message.Tool.{CodeInterpreterTool, FileSearchTool, FunctionTool}
-import sttp.openai.requests.completions.Usage
 import sttp.openai.requests.completions.chat.message.ToolResource.CodeInterpreterToolResource
 import sttp.openai.requests.completions.chat.message.ToolResources
 import sttp.openai.requests.threads.ThreadsRequestBody.CreateThreadBody
 import sttp.openai.requests.threads.messages.ThreadMessagesRequestBody.CreateMessage
 import sttp.openai.requests.threads.runs.ThreadRunsRequestBody.ToolOutput
-import sttp.openai.requests.threads.runs.ThreadRunsResponseData.{ListRunStepsResponse, ListRunsResponse, MessageCreation, RunStepData}
+import sttp.openai.requests.threads.runs.ThreadRunsResponseData.{
+  ListRunStepsResponse,
+  ListRunsResponse,
+  MessageCreation,
+  RunStepData,
+  Usage
+}
 import ujson.{Arr, Obj, Str}
 
 class ThreadRunsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
@@ -119,8 +124,8 @@ class ThreadRunsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
   }
 
   "Given list runs response as Json" should "be properly deserialized to case class" in {
-    import sttp.openai.requests.threads.runs.ThreadRunsResponseData.RunData
     import sttp.openai.requests.threads.runs.ThreadRunsResponseData.ListRunsResponse._
+    import sttp.openai.requests.threads.runs.ThreadRunsResponseData.RunData
 
     // given
     val jsonResponse = fixtures.ThreadRunsFixture.jsonListRunsResponse