Repository: softwaremill/sttp-openai
Pull request #275 (2025-02-07 00:07:10)

Update client4:akka-http-backend, ... to 4.0.0-RC1
## About this PR
📦 Updates 
* [com.softwaremill.sttp.client4:akka-http-backend](https://github.com/softwaremill/sttp)
* [com.softwaremill.sttp.client4:core](https://github.com/softwaremill/sttp)
* [com.softwaremill.sttp.client4:fs2](https://github.com/softwaremill/sttp)
* [com.softwaremill.sttp.client4:ox](https://github.com/softwaremill/sttp)
* [com.softwaremill.sttp.client4:pekko-http-backend](https://github.com/softwaremill/sttp)
* [com.softwaremill.sttp.client4:upickle](https://github.com/softwaremill/sttp)
* [com.softwaremill.sttp.client4:zio](https://github.com/softwaremill/sttp)

 from `4.0.0-M19` to `4.0.0-RC1`

📜 [GitHub Release Notes](https://github.com/softwaremill/sttp/releases/tag/v4.0.0-RC1) - [Version Diff](https://github.com/softwaremill/sttp/compare/v4.0.0-M19...v4.0.0-RC1)

## Usage
✅ **Please merge!**

I'll automatically update this PR to resolve conflicts as long as you don't change it yourself.

If you'd like to skip this version, you can just close this PR. If you have any feedback, just mention me in the comments below.

Configure Scala Steward for your repository with a [`.scala-steward.conf`](https://github.com/scala-steward-org/scala-steward/blob/8929fadb6f535472bd63397b1cb55a835fa1b5cb/docs/repo-specific-configuration.md) file.

_Have a fantastic day writing Scala!_

<details>
<summary>🔍 Files still referring to the old version number</summary>

The following files still refer to the old version number (4.0.0-M19).
You might want to review and update them manually.
```
examples/src/main/scala/examples/ChatProxy.scala
```
</details>
<details>
<summary>⚙ Adjust future updates</summary>

Add this to your `.scala-steward.conf` file to ignore future updates of this dependency:
```
updates.ignore = [ { groupId = "com.softwaremill.sttp.client4" } ]
```
Or, add this to slow down future updates of this dependency:
```
dependencyOverrides = [{
  pullRequests = { frequency = "30 days" },
  dependency = { groupId = "com.softwaremill.sttp.client4" }
}]
```
</details>

<sup>
labels: library-update, early-semver-pre-release, semver-spec-pre-release, old-version-remains, commit-count:1
</sup>

<!-- scala-steward = {
  "Update" : {
    "ForGroupId" : {
      "forArtifactIds" : [
        {
          "ForArtifactId" : {
            "crossDependency" : [
              {
                "groupId" : "com.softwaremill.sttp.client4",
                "artifactId" : {
                  "name" : "akka-http-backend",
                  "maybeCrossName" : "akka-http-backend_2.13"
                },
                "version" : "4.0.0-M19",
                "sbtVersion" : null,
                "scalaVersion" : null,
                "configurations" : null
              }
            ],
            "newerVersions" : [
              "4.0.0-RC1"
            ],
            "newerGroupId" : null,
            "newerArtifactId" : null
          }
        },
        {
          "ForArtifactId" : {
            "crossDependency" : [
              {
                "groupId" : "com.softwaremill.sttp.client4",
                "artifactId" : {
                  "name" : "core",
                  "maybeCrossName" : "core_2.13"
                },
                "version" : "4.0.0-M19",
                "sbtVersion" : null,
                "scalaVersion" : null,
                "configurations" : null
              },
              {
                "groupId" : "com.softwaremill.sttp.client4",
                "artifactId" : {
                  "name" : "core",
                  "maybeCrossName" : "core_3"
                },
                "version" : "4.0.0-M19",
                "sbtVersion" : null,
                "scalaVersion" : null,
                "configurations" : null
              }
            ],
            "newerVersions" : [
              "4.0.0-RC1"
            ],
            "newerGroupId" : null,
            "newerArtifactId" : null
          }
        },
        {
          "ForArtifactId" : {
            "crossDependency" : [
              {
                "groupId" : "com.softwaremill.sttp.client4",
                "artifactId" : {
                  "name" : "fs2",
                  "maybeCrossName" : "fs2_2.13"
                },
                "version" : "4.0.0-M19",
                "sbtVersion" : null,
                "scalaVersion" : null,
                "configurations" : null
              },
              {
                "groupId" : "com.softwaremill.sttp.client4",
                "artifactId" : {
                  "name" : "fs2",
                  "maybeCrossName" : "fs2_3"
                },
                "version" : "4.0.0-M19",
                "sbtVersion" : null,
                "scalaVersion" : null,
                "configurations" : null
              }
            ],
            "newerVersions" : [
              "4.0.0-RC1"
            ],
            "newerGroupId" : null,
            "newerArtifactId" : null
          }
        },
        {
          "ForArtifactId" : {
            "crossDependency" : [
              {
                "groupId" : "com.softwaremill.sttp.client4",
                "artifactId" : {
                  "name" : "ox",
                  "maybeCrossName" : "ox_3"
                },
                "version" : "4.0.0-M19",
                "sbtVersion" : null,
                "scalaVersion" : null,
                "configurations" : null
              },
              {
                "groupId" : "com.softwaremill.sttp.client4",
                "artifactId" : {
                  "name" : "ox",
                  "maybeCrossName" : "ox_3"
                },
                "version" : "4.0.0-M19",
                "sbtVersion" : null,
                "scalaVersion" : null,
                "configurations" : "test"
              }
            ],
            "newerVersions" : [
              "4.0.0-RC1"
            ],
            "newerGroupId" : null,
            "newerArtifactId" : null
          }
        },
        {
          "ForArtifactId" : {
            "crossDependency" : [
              {
                "groupId" : "com.softwaremill.sttp.client4",
                "artifactId" : {
                  "name" : "pekko-http-backend",
                  "maybeCrossName" : "pekko-http-backend_2.13"
                },
                "version" : "4.0.0-M19",
                "sbtVersion" : null,
                "scalaVersion" : null,
                "configurations" : null
              },
              {
                "groupId" : "com.softwaremill.sttp.client4",
                "artifactId" : {
                  "name" : "pekko-http-backend",
                  "maybeCrossName" : "pekko-http-backend_3"
                },
                "version" : "4.0.0-M19",
                "sbtVersion" : null,
                "scalaVersion" : null,
                "configurations" : null
              }
            ],
            "newerVersions" : [
              "4.0.0-RC1"
            ],
            "newerGroupId" : null,
            "newerArtifactId" : null
          }
        },
        {
          "ForArtifactId" : {
            "crossDependency" : [
              {
                "groupId" : "com.softwaremill.sttp.client4",
                "artifactId" : {
                  "name" : "upickle",
                  "maybeCrossName" : "upickle_2.13"
                },
                "version" : "4.0.0-M19",
                "sbtVersion" : null,
                "scalaVersion" : null,
                "configurations" : null
              },
              {
                "groupId" : "com.softwaremill.sttp.client4",
                "artifactId" : {
                  "name" : "upickle",
                  "maybeCrossName" : "upickle_3"
                },
                "version" : "4.0.0-M19",
                "sbtVersion" : null,
                "scalaVersion" : null,
                "configurations" : null
              }
            ],
            "newerVersions" : [
              "4.0.0-RC1"
            ],
            "newerGroupId" : null,
            "newerArtifactId" : null
          }
        },
        {
          "ForArtifactId" : {
            "crossDependency" : [
              {
                "groupId" : "com.softwaremill.sttp.client4",
                "artifactId" : {
                  "name" : "zio",
                  "maybeCrossName" : "zio_2.13"
                },
                "version" : "4.0.0-M19",
                "sbtVersion" : null,
                "scalaVersion" : null,
                "configurations" : null
              },
              {
                "groupId" : "com.softwaremill.sttp.client4",
                "artifactId" : {
                  "name" : "zio",
                  "maybeCrossName" : "zio_3"
                },
                "version" : "4.0.0-M19",
                "sbtVersion" : null,
                "scalaVersion" : null,
                "configurations" : null
              }
            ],
            "newerVersions" : [
              "4.0.0-RC1"
            ],
            "newerGroupId" : null,
            "newerArtifactId" : null
          }
        }
      ]
    }
  },
  "Labels" : [
    "library-update",
    "early-semver-pre-release",
    "semver-spec-pre-release",
    "old-version-remains",
    "commit-count:1"
  ]
} -->


Patch:
diff --git a/core/src/main/scala/sttp/openai/OpenAI.scala b/core/src/main/scala/sttp/openai/OpenAI.scala
index 11f1985..c9f237d 100644
--- a/core/src/main/scala/sttp/openai/OpenAI.scala
+++ b/core/src/main/scala/sttp/openai/OpenAI.scala
@@ -4,13 +4,7 @@ import sttp.capabilities.Streams
 import sttp.client4._
 import sttp.model.{Header, Uri}
 import sttp.openai.OpenAIExceptions.OpenAIException
-import sttp.openai.json.SttpUpickleApiExtension.{
-  asInputStreamUnsafe_parseErrors,
-  asJson_parseErrors,
-  asStreamUnsafe_parseErrors,
-  asStringEither,
-  upickleBodySerializer
-}
+import sttp.openai.json.SttpUpickleApiExtension._
 import sttp.openai.requests.admin.{QueryParameters => _, _}
 import sttp.openai.requests.assistants.AssistantsRequestBody.{CreateAssistantBody, ModifyAssistantBody}
 import sttp.openai.requests.assistants.AssistantsResponseData.{AssistantData, DeleteAssistantResponse, ListAssistantsResponse}
@@ -99,7 +93,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   def createCompletion(completionBody: CompletionsBody): Request[Either[OpenAIException, CompletionsResponse]] =
     openAIAuthRequest
       .post(openAIUris.Completions)
-      .body(completionBody)
+      .body(asJson(completionBody))
       .response(asJson_parseErrors[CompletionsResponse])
 
   /** Creates an image given a prompt in request body.
@@ -112,7 +106,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   def createImage(imageCreationBody: ImageCreationBody): Request[Either[OpenAIException, ImageResponse]] =
     openAIAuthRequest
       .post(openAIUris.CreateImage)
-      .body(imageCreationBody)
+      .body(asJson(imageCreationBody))
       .response(asJson_parseErrors[ImageResponse])
 
   /** Creates edited or extended images given an original image and a prompt.
@@ -175,7 +169,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
           Some(multipartFile("image", image)),
           Some(multipart("prompt", prompt)),
           mask.map(multipartFile("mask", _)),
-          n.map(multipart("n", _)),
+          n.map(i => multipart("n", i.toString)),
           size.map(s => multipart("size", s.value)),
           responseFormat.map(format => multipart("response_format", format.value))
         ).flatten
@@ -236,7 +230,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
         import imageVariationsConfig._
         Seq(
           Some(multipartFile("image", image)),
-          n.map(multipart("n", _)),
+          n.map(i => multipart("n", i.toString)),
           size.map(s => multipart("size", s.value)),
           responseFormat.map(format => multipart("response_format", format.value)),
           user.map(multipart("user", _))
@@ -254,7 +248,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   def createChatCompletion(chatBody: ChatBody): Request[Either[OpenAIException, ChatResponse]] =
     openAIAuthRequest
       .post(openAIUris.ChatCompletions)
-      .body(chatBody)
+      .body(asJson(chatBody))
       .response(asJson_parseErrors[ChatResponse])
 
   /** Creates a model response for the given chat conversation defined in chatBody.
@@ -272,7 +266,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   def createChatCompletionAsBinaryStream[S](s: Streams[S], chatBody: ChatBody): StreamRequest[Either[OpenAIException, s.BinaryStream], S] =
     openAIAuthRequest
       .post(openAIUris.ChatCompletions)
-      .body(ChatBody.withStreaming(chatBody))
+      .body(asJson(ChatBody.withStreaming(chatBody)))
       .response(asStreamUnsafe_parseErrors(s))
 
   /** Creates a model response for the given chat conversation defined in chatBody.
@@ -287,7 +281,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   def createChatCompletionAsInputStream(chatBody: ChatBody): Request[Either[OpenAIException, InputStream]] =
     openAIAuthRequest
       .post(openAIUris.ChatCompletions)
-      .body(ChatBody.withStreaming(chatBody))
+      .body(asJson(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.
@@ -366,7 +360,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   ): Request[Either[OpenAIException, ChatResponse]] =
     openAIAuthRequest
       .post(openAIUris.chatCompletion(completionId))
-      .body(requestBody)
+      .body(asJson(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.
@@ -546,7 +540,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
       .post(openAIUris.Translations)
       .multipartBody(
         multipartFile("file", Paths.get(systemPath).toFile),
-        multipart("model", model)
+        multipart("model", asJson(model))
       )
       .response(asJson_parseErrors[AudioResponse])
 
@@ -566,8 +560,8 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
           Some(multipartFile("file", file)),
           Some(multipart("model", model.value)),
           prompt.map(multipart("prompt", _)),
-          responseFormat.map(format => multipart("response_format", format)),
-          temperature.map(multipart("temperature", _)),
+          responseFormat.map(format => multipart("response_format", asJson(format))),
+          temperature.map(i => multipart("temperature", i.toString)),
           language.map(multipart("language", _))
         ).flatten
       }
@@ -583,7 +577,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   def createModeration(moderationsBody: ModerationsBody): Request[Either[OpenAIException, ModerationData]] =
     openAIAuthRequest
       .post(openAIUris.Moderations)
-      .body(moderationsBody)
+      .body(asJson(moderationsBody))
       .response(asJson_parseErrors[ModerationData])
 
   /** Transcribes audio into the input language.
@@ -642,7 +636,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
           Some(multipart("model", model.value)),
           prompt.map(multipart("prompt", _)),
           responseFormat.map(format => multipart("response_format", format.value)),
-          temperature.map(multipart("temperature", _)),
+          temperature.map(t => multipart("temperature", t.toString)),
           language.map(lang => multipart("language", lang.value))
         ).flatten
       }
@@ -672,7 +666,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   def createUpload(uploadRequestBody: UploadRequestBody): Request[Either[OpenAIException, UploadResponse]] =
     openAIAuthRequest
       .post(openAIUris.Uploads)
-      .body(uploadRequestBody)
+      .body(asJson(uploadRequestBody))
       .response(asJson_parseErrors[UploadResponse])
 
   /** Adds a Part to an Upload object. A Part represents a chunk of bytes from the file you are trying to upload.
@@ -719,7 +713,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   def completeUpload(uploadId: String, requestBody: CompleteUploadRequestBody): Request[Either[OpenAIException, UploadResponse]] =
     openAIAuthRequest
       .post(openAIUris.completeUpload(uploadId))
-      .body(requestBody)
+      .body(asJson(requestBody))
       .response(asJson_parseErrors[UploadResponse])
 
   /** Cancels the Upload. No Parts may be added after an Upload is cancelled.
@@ -749,7 +743,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   def createFineTuningJob(fineTuningRequestBody: FineTuningJobRequestBody): Request[Either[OpenAIException, FineTuningJobResponse]] =
     openAIAuthRequest
       .post(openAIUris.FineTuningJobs)
-      .body(fineTuningRequestBody)
+      .body(asJson(fineTuningRequestBody))
       .response(asJson_parseErrors[FineTuningJobResponse])
 
   /** List your organization's fine-tuning jobs
@@ -841,7 +835,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   def createEmbeddings(embeddingsBody: EmbeddingsBody): Request[Either[OpenAIException, EmbeddingResponse]] =
     openAIAuthRequest
       .post(openAIUris.Embeddings)
-      .body(embeddingsBody)
+      .body(asJson(embeddingsBody))
       .response(asJson_parseErrors[EmbeddingResponse])
 
   /** Create a thread.
@@ -854,7 +848,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   def createThread(createThreadBody: CreateThreadBody): Request[Either[OpenAIException, ThreadData]] =
     betaOpenAIAuthRequest
       .post(openAIUris.Threads)
-      .body(createThreadBody)
+      .body(asJson(createThreadBody))
       .response(asJson_parseErrors[ThreadData])
 
   /** Retrieves a thread.
@@ -904,7 +898,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   def createThreadMessage(threadId: String, message: CreateMessage): Request[Either[OpenAIException, MessageData]] =
     betaOpenAIAuthRequest
       .post(openAIUris.threadMessages(threadId))
-      .body(message)
+      .body(asJson(message))
       .response(asJson_parseErrors[MessageData])
 
   /** Returns a list of messages for a given thread.
@@ -989,7 +983,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   def createAssistant(createAssistantBody: CreateAssistantBody): Request[Either[OpenAIException, AssistantData]] =
     betaOpenAIAuthRequest
       .post(openAIUris.Assistants)
-      .body(createAssistantBody)
+      .body(asJson(createAssistantBody))
       .response(asJson_parseErrors[AssistantData])
 
   /** Returns a list of assistants.
@@ -1032,7 +1026,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   def modifyAssistant(assistantId: String, modifyAssistantBody: ModifyAssistantBody): Request[Either[OpenAIException, AssistantData]] =
     betaOpenAIAuthRequest
       .post(openAIUris.assistant(assistantId))
-      .body(modifyAssistantBody)
+      .body(asJson(modifyAssistantBody))
       .response(asJson_parseErrors[AssistantData])
 
   /** Delete an assistant.
@@ -1059,7 +1053,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   def createRun(threadId: String, createRun: CreateRun): Request[Either[OpenAIException, RunData]] =
     betaOpenAIAuthRequest
       .post(openAIUris.threadRuns(threadId))
-      .body(createRun)
+      .body(asJson(createRun))
       .response(asJson_parseErrors[RunData])
 
   /** Create a thread and run it in one request.
@@ -1072,7 +1066,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   def createThreadAndRun(createThreadAndRun: CreateThreadAndRun): Request[Either[OpenAIException, RunData]] =
     betaOpenAIAuthRequest
       .post(openAIUris.ThreadsRuns)
-      .body(createThreadAndRun)
+      .body(asJson(createThreadAndRun))
       .response(asJson_parseErrors[RunData])
 
   /** Returns a list of runs belonging to a thread..
@@ -1157,7 +1151,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   def modifyRun(threadId: String, runId: String, metadata: Map[String, String]): Request[Either[OpenAIException, RunData]] =
     betaOpenAIAuthRequest
       .post(openAIUris.threadRun(threadId, runId))
-      .body(ModifyRun(metadata))
+      .body(asJson(ModifyRun(metadata)))
       .response(asJson_parseErrors[RunData])
 
   /** When a run has the status: "requires_action" and required_action.type is submit_tool_outputs, this endpoint can be used to submit the
@@ -1175,7 +1169,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   def submitToolOutputs(threadId: String, runId: String, toolOutputs: Seq[ToolOutput]): Request[Either[OpenAIException, RunData]] =
     betaOpenAIAuthRequest
       .post(openAIUris.threadRunSubmitToolOutputs(threadId, runId))
-      .body(SubmitToolOutputsToRun(toolOutputs))
+      .body(asJson(SubmitToolOutputsToRun(toolOutputs)))
       .response(asJson_parseErrors[RunData])
 
   /** Cancels a run that is in_progress.
@@ -1203,7 +1197,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   def createVectorStore(createVectorStoreBody: CreateVectorStoreBody): Request[Either[OpenAIException, VectorStore]] =
     betaOpenAIAuthRequest
       .post(openAIUris.VectorStores)
-      .body(createVectorStoreBody)
+      .body(asJson(createVectorStoreBody))
       .response(asJson_parseErrors[VectorStore])
 
   /** Lists vector store
@@ -1247,7 +1241,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   ): Request[Either[OpenAIException, VectorStore]] =
     betaOpenAIAuthRequest
       .post(openAIUris.vectorStore(vectorStoreId))
-      .body(modifyVectorStoreBody)
+      .body(asJson(modifyVectorStoreBody))
       .response(asJson_parseErrors[VectorStore])
 
   /** Deletes vector store
@@ -1277,7 +1271,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   ): Request[Either[OpenAIException, VectorStoreFile]] =
     betaOpenAIAuthRequest
       .post(openAIUris.vectorStoreFiles(vectorStoreId))
-      .body(createVectorStoreFileBody)
+      .body(asJson(createVectorStoreFileBody))
       .response(asJson_parseErrors[VectorStoreFile])
 
   /** List files belonging to particular datastore
@@ -1337,7 +1331,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   def createBatch(createBatchRequest: BatchRequestBody): Request[Either[OpenAIException, BatchResponse]] =
     openAIAuthRequest
       .post(openAIUris.Batches)
-      .body(createBatchRequest)
+      .body(asJson(createBatchRequest))
       .response(asJson_parseErrors[BatchResponse])
 
   /** Retrieves a batch.
@@ -1399,7 +1393,7 @@ class OpenAI(authToken: String, baseUri: Uri = OpenAIUris.OpenAIBaseUri) {
   def createAdminApiKey(createAdminApiKeyRequest: AdminApiKeyRequestBody): Request[Either[OpenAIException, AdminApiKeyResponse]] =
     openAIAuthRequest
       .post(openAIUris.AdminApiKeys)
-      .body(createAdminApiKeyRequest)
+      .body(asJson(createAdminApiKeyRequest))
       .response(asJson_parseErrors[AdminApiKeyResponse])
 
   /** Retrieve a single organization API key
diff --git a/core/src/main/scala/sttp/openai/OpenAIExceptions.scala b/core/src/main/scala/sttp/openai/OpenAIExceptions.scala
index 5bd7819..7371c54 100644
--- a/core/src/main/scala/sttp/openai/OpenAIExceptions.scala
+++ b/core/src/main/scala/sttp/openai/OpenAIExceptions.scala
@@ -1,6 +1,8 @@
 package sttp.openai
 
-import sttp.client4.{DeserializationException, HttpError, ResponseException}
+import sttp.client4.ResponseException
+import sttp.client4.ResponseException.{DeserializationException, UnexpectedStatusCode}
+import sttp.model.ResponseMetadata
 
 object OpenAIExceptions {
   sealed abstract class OpenAIException(
@@ -8,27 +10,29 @@ object OpenAIExceptions {
       val `type`: Option[String],
       val param: Option[String],
       val code: Option[String],
-      val cause: ResponseException[String, Exception]
+      val cause: ResponseException[String]
   ) extends Exception(cause.getMessage, cause)
 
   object OpenAIException {
     class DeserializationOpenAIException(
         message: String,
-        cause: DeserializationException[Exception]
+        cause: DeserializationException
     ) extends OpenAIException(Some(message), None, None, None, cause)
 
     object DeserializationOpenAIException {
-      def apply(cause: DeserializationException[Exception]): DeserializationOpenAIException =
+      def apply(cause: DeserializationException): DeserializationOpenAIException =
         new DeserializationOpenAIException(cause.getMessage, cause)
 
-      def apply(cause: Exception): DeserializationOpenAIException = apply(DeserializationException(cause.getMessage, cause))
+      def apply(cause: Exception, meta: ResponseMetadata): DeserializationOpenAIException = apply(
+        DeserializationException(cause.getMessage, cause, meta)
+      )
     }
     class RateLimitException(
         message: Option[String],
         `type`: Option[String],
         param: Option[String],
         code: Option[String],
-        cause: HttpError[String]
+        cause: UnexpectedStatusCode[String]
     ) extends OpenAIException(message, `type`, param, code, cause)
 
     class InvalidRequestException(
@@ -36,7 +40,7 @@ object OpenAIExceptions {
         `type`: Option[String],
         param: Option[String],
         code: Option[String],
-        cause: HttpError[String]
+        cause: UnexpectedStatusCode[String]
     ) extends OpenAIException(message, `type`, param, code, cause)
 
     class AuthenticationException(
@@ -44,7 +48,7 @@ object OpenAIExceptions {
         `type`: Option[String],
         param: Option[String],
         code: Option[String],
-        cause: HttpError[String]
+        cause: UnexpectedStatusCode[String]
     ) extends OpenAIException(message, `type`, param, code, cause)
 
     class PermissionException(
@@ -52,7 +56,7 @@ object OpenAIExceptions {
         `type`: Option[String],
         param: Option[String],
         code: Option[String],
-        cause: HttpError[String]
+        cause: UnexpectedStatusCode[String]
     ) extends OpenAIException(message, `type`, param, code, cause)
 
     class TryAgain(
@@ -60,7 +64,7 @@ object OpenAIExceptions {
         `type`: Option[String],
         param: Option[String],
         code: Option[String],
-        cause: HttpError[String]
+        cause: UnexpectedStatusCode[String]
     ) extends OpenAIException(message, `type`, param, code, cause)
 
     class ServiceUnavailableException(
@@ -68,7 +72,7 @@ object OpenAIExceptions {
         `type`: Option[String],
         param: Option[String],
         code: Option[String],
-        cause: HttpError[String]
+        cause: UnexpectedStatusCode[String]
     ) extends OpenAIException(message, `type`, param, code, cause)
 
     class APIException(
@@ -76,7 +80,7 @@ object OpenAIExceptions {
         `type`: Option[String],
         param: Option[String],
         code: Option[String],
-        cause: HttpError[String]
+        cause: UnexpectedStatusCode[String]
     ) extends OpenAIException(message, `type`, param, code, cause)
   }
 }
diff --git a/core/src/main/scala/sttp/openai/json/SttpUpickleApiExtension.scala b/core/src/main/scala/sttp/openai/json/SttpUpickleApiExtension.scala
index 4efb393..1acfd05 100644
--- a/core/src/main/scala/sttp/openai/json/SttpUpickleApiExtension.scala
+++ b/core/src/main/scala/sttp/openai/json/SttpUpickleApiExtension.scala
@@ -1,13 +1,14 @@
 package sttp.openai.json
 
+import sttp.capabilities.Streams
+import sttp.client4.ResponseException.UnexpectedStatusCode
+import sttp.client4._
 import sttp.client4.json._
 import sttp.client4.upicklejson.SttpUpickleApi
-import sttp.client4._
-import sttp.model.StatusCode._
 import sttp.model.ResponseMetadata
+import sttp.model.StatusCode._
 import sttp.openai.OpenAIExceptions.OpenAIException
 import sttp.openai.OpenAIExceptions.OpenAIException._
-import sttp.capabilities.Streams
 
 import java.io.InputStream
 
@@ -19,52 +20,53 @@ object SttpUpickleApiExtension extends SttpUpickleApi {
 
   def asStreamUnsafe_parseErrors[S](s: Streams[S]): StreamResponseAs[Either[OpenAIException, s.BinaryStream], S] =
     asStreamUnsafe(s).mapWithMetadata { (body, meta) =>
-      body.left.map(errorBody => httpToOpenAIError(HttpError(errorBody, meta.code)))
+      body.left.map(errorBody => httpToOpenAIError(UnexpectedStatusCode(errorBody, meta)))
     }
 
   def asInputStreamUnsafe_parseErrors: ResponseAs[Either[OpenAIException, InputStream]] =
     asInputStreamUnsafe.mapWithMetadata { (body, meta) =>
-      body.left.map(errorBody => httpToOpenAIError(HttpError(errorBody, meta.code)))
+      body.left.map(errorBody => httpToOpenAIError(UnexpectedStatusCode(errorBody, meta)))
     }
 
   def asJson_parseErrors[B: upickleApi.Reader: IsOption]: ResponseAs[Either[OpenAIException, B]] =
     asString.mapWithMetadata(deserializeRightWithMappedExceptions(deserializeJsonSnake)).showAsJson
 
   private def deserializeRightWithMappedExceptions[T](
-      doDeserialize: String => Either[DeserializationOpenAIException, T]
+      doDeserialize: (String, ResponseMetadata) => Either[DeserializationOpenAIException, T]
   ): (Either[String, String], ResponseMetadata) => Either[OpenAIException, T] = {
     case (Left(body), meta) =>
-      Left(httpToOpenAIError(HttpError(body, meta.code)))
-    case (Right(body), _) => doDeserialize.apply(body)
+      Left(httpToOpenAIError(UnexpectedStatusCode(body, meta)))
+    case (Right(body), meta) => doDeserialize.apply(body, meta)
   }
 
-  def deserializeJsonSnake[B: upickleApi.Reader: IsOption]: String => Either[DeserializationOpenAIException, B] = { (s: String) =>
-    try
-      Right(upickleApi.read[B](JsonInput.sanitize[B].apply(s)))
-    catch {
-      case e: Exception => Left(DeserializationOpenAIException(e))
-      case t: Throwable =>
-        // in ScalaJS, ArrayIndexOutOfBoundsException exceptions are wrapped in org.scalajs.linker.runtime.UndefinedBehaviorError
-        t.getCause match {
-          case e: ArrayIndexOutOfBoundsException => Left(DeserializationOpenAIException(e))
-          case _                                 => throw t
-        }
-    }
+  def deserializeJsonSnake[B: upickleApi.Reader: IsOption]: (String, ResponseMetadata) => Either[DeserializationOpenAIException, B] = {
+    (s: String, meta: ResponseMetadata) =>
+      try
+        Right(upickleApi.read[B](JsonInput.sanitize[B].apply(s)))
+      catch {
+        case e: Exception => Left(DeserializationOpenAIException(e, meta))
+        case t: Throwable =>
+          // in ScalaJS, ArrayIndexOutOfBoundsException exceptions are wrapped in org.scalajs.linker.runtime.UndefinedBehaviorError
+          t.getCause match {
+            case e: ArrayIndexOutOfBoundsException => Left(DeserializationOpenAIException(e, meta))
+            case _                                 => throw t
+          }
+      }
   }
 
   def asStringEither: ResponseAs[Either[OpenAIException, String]] =
     asStringAlways
       .mapWithMetadata { (string, metadata) =>
-        if (metadata.isSuccess) Right(string) else Left(httpToOpenAIError(HttpError(string, metadata.code)))
+        if (metadata.isSuccess) Right(string) else Left(httpToOpenAIError(UnexpectedStatusCode(string, metadata)))
       }
       .showAs("either(as error, as string)")
 
-  private def httpToOpenAIError(he: HttpError[String]): OpenAIException = {
+  private def httpToOpenAIError(he: UnexpectedStatusCode[String]): OpenAIException = {
     val errorMessageBody = upickleApi.read[ujson.Value](he.body).apply("error")
     val error = upickleApi.read[Error](errorMessageBody)
     import error._
 
-    he.statusCode match {
+    he.response.code match {
       case TooManyRequests                              => new RateLimitException(message, `type`, param, code, he)
       case BadRequest | NotFound | UnsupportedMediaType => new InvalidRequestException(message, `type`, param, code, he)
       case Unauthorized                                 => new AuthenticationException(message, `type`, param, code, he)
diff --git a/core/src/main/scala/sttp/openai/requests/completions/CompletionsRequestBody.scala b/core/src/main/scala/sttp/openai/requests/completions/CompletionsRequestBody.scala
index 22fc226..1b363b9 100644
--- a/core/src/main/scala/sttp/openai/requests/completions/CompletionsRequestBody.scala
+++ b/core/src/main/scala/sttp/openai/requests/completions/CompletionsRequestBody.scala
@@ -1,6 +1,5 @@
 package sttp.openai.requests.completions
 
-import sttp.openai.OpenAIExceptions.OpenAIException.DeserializationOpenAIException
 import sttp.openai.json.SnakePickle
 import ujson.Str
 
@@ -78,7 +77,7 @@ object CompletionsRequestBody {
           SnakePickle.read[ujson.Value](jsonValue) match {
             case Str(value) =>
               byCompletionModelValue.getOrElse(value, CustomCompletionModel(value))
-            case e => throw DeserializationOpenAIException(new Exception(s"Could not deserialize: $e"))
+            case e => throw new Exception(s"Could not deserialize: $e")
           }
       )
 
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 61c9df0..d99ace0 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
@@ -4,7 +4,6 @@ import io.circe.syntax._
 import io.circe.{DecodingFailure, Json, JsonNumber, JsonObject}
 import sttp.apispec.Schema
 import sttp.apispec.circe._
-import sttp.openai.OpenAIExceptions.OpenAIException.DeserializationOpenAIException
 import sttp.openai.json.SnakePickle
 import sttp.openai.requests.completions.Stop
 import sttp.openai.requests.completions.chat.message.{Message, Tool, ToolChoice}
@@ -405,7 +404,7 @@ object ChatRequestBody {
           SnakePickle.read[ujson.Value](jsonValue) match {
             case Str(value) =>
               byChatModelValue.getOrElse(value, CustomChatCompletionModel(value))
-            case e => throw DeserializationOpenAIException(new Exception(s"Could not deserialize: $e"))
+            case e => throw new Exception(s"Could not deserialize: $e")
           }
       )
 
diff --git a/core/src/main/scala/sttp/openai/requests/completions/chat/Role.scala b/core/src/main/scala/sttp/openai/requests/completions/chat/Role.scala
index 460ec74..5508dce 100644
--- a/core/src/main/scala/sttp/openai/requests/completions/chat/Role.scala
+++ b/core/src/main/scala/sttp/openai/requests/completions/chat/Role.scala
@@ -1,6 +1,5 @@
 package sttp.openai.requests.completions.chat
 
-import sttp.openai.OpenAIExceptions.OpenAIException.DeserializationOpenAIException
 import sttp.openai.json.SnakePickle
 import ujson.Str
 
@@ -28,7 +27,7 @@ object Role {
       jsonValue =>
         SnakePickle.read[ujson.Value](jsonValue) match {
           case Str(value) => byRoleValue.getOrElse(value, Custom(value))
-          case e          => throw DeserializationOpenAIException(new Exception(s"Could not deserialize: $e"))
+          case e          => throw new Exception(s"Could not deserialize: $e")
         }
     )
 }
diff --git a/core/src/main/scala/sttp/openai/requests/completions/chat/message/Content.scala b/core/src/main/scala/sttp/openai/requests/completions/chat/message/Content.scala
index 9ec474c..7754d93 100644
--- a/core/src/main/scala/sttp/openai/requests/completions/chat/message/Content.scala
+++ b/core/src/main/scala/sttp/openai/requests/completions/chat/message/Content.scala
@@ -1,6 +1,5 @@
 package sttp.openai.requests.completions.chat.message
 
-import sttp.openai.OpenAIExceptions.OpenAIException.DeserializationOpenAIException
 import sttp.openai.json.SnakePickle
 import ujson._
 
@@ -21,7 +20,7 @@ object Content {
         SnakePickle.read[Value](jsonValue) match {
           case Str(value) => TextContent(value)
           case Arr(value) => ArrayContent(value.toSeq.map(SnakePickle.read[ContentPart](_)))
-          case e          => throw DeserializationOpenAIException(new Exception(s"Could not deserialize: $e"))
+          case e          => throw new Exception(s"Could not deserialize: $e")
         }
     )
 
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 59635b0..cfcd3aa 100644
--- a/core/src/main/scala/sttp/openai/requests/embeddings/EmbeddingsRequestBody.scala
+++ b/core/src/main/scala/sttp/openai/requests/embeddings/EmbeddingsRequestBody.scala
@@ -1,6 +1,5 @@
 package sttp.openai.requests.embeddings
 
-import sttp.openai.OpenAIExceptions.OpenAIException.DeserializationOpenAIException
 import sttp.openai.json.SnakePickle
 import ujson.Str
 
@@ -33,7 +32,7 @@ object EmbeddingsRequestBody {
           SnakePickle.read[ujson.Value](jsonValue) match {
             case Str(value) =>
               byEmbeddingsModelValue.getOrElse(value, CustomEmbeddingsModel(value))
-            case e => throw DeserializationOpenAIException(new Exception(s"Could not deserialize: $e"))
+            case e => throw new Exception(s"Could not deserialize: $e")
           }
       )
     case object TextEmbeddingAda002 extends EmbeddingsModel("text-embedding-ada-002")
diff --git a/core/src/main/scala/sttp/openai/requests/finetuning/FineTuningJobRequestBody.scala b/core/src/main/scala/sttp/openai/requests/finetuning/FineTuningJobRequestBody.scala
index a46d147..d7a12b7 100644
--- a/core/src/main/scala/sttp/openai/requests/finetuning/FineTuningJobRequestBody.scala
+++ b/core/src/main/scala/sttp/openai/requests/finetuning/FineTuningJobRequestBody.scala
@@ -1,6 +1,5 @@
 package sttp.openai.requests.finetuning
 
-import sttp.openai.OpenAIExceptions.OpenAIException.DeserializationOpenAIException
 import sttp.openai.json.SnakePickle
 import ujson.Str
 
@@ -53,7 +52,7 @@ object FineTuningModel {
         SnakePickle.read[ujson.Value](jsonValue) match {
           case Str(value) =>
             byFineTuningModelValue.getOrElse(value, CustomFineTuningModel(value))
-          case e => throw DeserializationOpenAIException(new Exception(s"Could not deserialize: $e"))
+          case e => throw new Exception(s"Could not deserialize: $e")
         }
     )
 
diff --git a/core/src/main/scala/sttp/openai/requests/finetuning/FineTuningJobResponse.scala b/core/src/main/scala/sttp/openai/requests/finetuning/FineTuningJobResponse.scala
index a528b2f..78ae27d 100644
--- a/core/src/main/scala/sttp/openai/requests/finetuning/FineTuningJobResponse.scala
+++ b/core/src/main/scala/sttp/openai/requests/finetuning/FineTuningJobResponse.scala
@@ -1,6 +1,5 @@
 package sttp.openai.requests.finetuning
 
-import sttp.openai.OpenAIExceptions.OpenAIException.DeserializationOpenAIException
 import sttp.openai.json.SnakePickle
 import ujson.Str
 
@@ -97,7 +96,7 @@ object Status {
     .map[Status](jsonValue =>
       SnakePickle.read[ujson.Value](jsonValue) match {
         case Str(value) => byStatusValue.getOrElse(value, CustomStatus(value))
-        case e          => throw DeserializationOpenAIException(new Exception(s"Could not deserialize: $e"))
+        case e          => throw new Exception(s"Could not deserialize: $e")
       }
     )
 
diff --git a/core/src/main/scala/sttp/openai/requests/finetuning/Type.scala b/core/src/main/scala/sttp/openai/requests/finetuning/Type.scala
index 03078e8..bbc343a 100644
--- a/core/src/main/scala/sttp/openai/requests/finetuning/Type.scala
+++ b/core/src/main/scala/sttp/openai/requests/finetuning/Type.scala
@@ -1,6 +1,5 @@
 package sttp.openai.requests.finetuning
 
-import sttp.openai.OpenAIExceptions.OpenAIException.DeserializationOpenAIException
 import sttp.openai.json.SnakePickle
 import ujson.Str
 
@@ -16,9 +15,9 @@ object Type {
           case Str(value) =>
             byTypeValue.get(value) match {
               case Some(t) => t
-              case None    => throw DeserializationOpenAIException(new Exception(s"Could not deserialize: $value"))
+              case None    => throw new Exception(s"Could not deserialize: $value")
             }
-          case e => throw DeserializationOpenAIException(new Exception(s"Could not deserialize: $e"))
+          case e => throw new Exception(s"Could not deserialize: $e")
         }
     )
 }
diff --git a/core/src/main/scala/sttp/openai/requests/moderations/ModerationsRequestBody.scala b/core/src/main/scala/sttp/openai/requests/moderations/ModerationsRequestBody.scala
index ddc980c..fc7650f 100644
--- a/core/src/main/scala/sttp/openai/requests/moderations/ModerationsRequestBody.scala
+++ b/core/src/main/scala/sttp/openai/requests/moderations/ModerationsRequestBody.scala
@@ -1,6 +1,5 @@
 package sttp.openai.requests.moderations
 
-import sttp.openai.OpenAIExceptions.OpenAIException.DeserializationOpenAIException
 import sttp.openai.json.SnakePickle
 import ujson.Str
 object ModerationsRequestBody {
@@ -25,7 +24,7 @@ object ModerationsRequestBody {
           SnakePickle.read[ujson.Value](jsonValue) match {
             case Str(value) =>
               byModerationModelValue.getOrElse(value, CustomModerationModel(value))
-            case e => throw DeserializationOpenAIException(new Exception(s"Could not deserialize: $e"))
+            case e => throw new Exception(s"Could not deserialize: $e")
           }
       )
 
diff --git a/examples/src/main/scala/examples/ChatProxy.scala b/examples/src/main/scala/examples/ChatProxy.scala
index 75a4bb8..e8cd31b 100644
--- a/examples/src/main/scala/examples/ChatProxy.scala
+++ b/examples/src/main/scala/examples/ChatProxy.scala
@@ -22,7 +22,6 @@ import sttp.openai.streaming.ox.*
 import sttp.tapir.*
 import sttp.tapir.CodecFormat.*
 import sttp.tapir.server.netty.sync.{NettySyncServer, OxStreams}
-
 import ox.flow.Flow
 
 //
@@ -47,7 +46,7 @@ def chat(sttpBackend: SyncBackend, openAI: OpenAI): OxStreams.Pipe[ChatMessage,
       incoming
         // main processing loop: receives messages from the WS and queries OpenAI with the chat's history
         .mapStateful(() => Vector.empty[Message]) { (history, nextMessage) =>
-          val nextHistory = history :+ Message.UserMessage(content = Content.TextContent(nextMessage.message))
+          val nextHistory = history.apply() :+ Message.UserMessage(content = Content.TextContent(nextMessage.message))
 
           // querying OpenAI with the entire chat history, as each request is stateless
           val chatRequestBody: ChatBody = ChatBody(
@@ -72,7 +71,7 @@ def chat(sttpBackend: SyncBackend, openAI: OpenAI): OxStreams.Pipe[ChatMessage,
           val entireResponse = responseList.map(_.message).mkString
           val nextNextHistory = nextHistory :+ Message.AssistantMessage(content = entireResponse)
 
-          (nextNextHistory, ())
+          (() => nextNextHistory, ())
         }
         // when the outer flow is run, running the incoming flow as well; it doesn't produce any meaningful results
         // (apart from emitting responses to the outer flow), so discarding its result
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index 2bf10bf..709611c 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -7,11 +7,11 @@ object Dependencies {
     val scalaTestCats = "1.6.0"
 
     val sttpApispec = "0.11.7"
-    val sttpClient = "4.0.0-M19"
+    val sttpClient = "4.0.0-RC1"
     val pekkoStreams = "1.1.3"
     val akkaStreams = "2.6.20"
     val tapir = "1.11.17"
-    val uPickle = "3.1.4"
+    val uPickle = "4.1.0"
   }
 
   object Libraries {
diff --git a/streaming/akka/src/main/scala/sttp/openai/streaming/akka/package.scala b/streaming/akka/src/main/scala/sttp/openai/streaming/akka/package.scala
index a951675..6a42624 100644
--- a/streaming/akka/src/main/scala/sttp/openai/streaming/akka/package.scala
+++ b/streaming/akka/src/main/scala/sttp/openai/streaming/akka/package.scala
@@ -4,13 +4,14 @@ import _root_.akka.stream.scaladsl.{Flow, Source}
 import _root_.akka.util.ByteString
 import sttp.capabilities.akka.AkkaStreams
 import sttp.client4.StreamRequest
+import sttp.client4.akkahttp.AkkaHttpServerSentEvents
+import sttp.model.ResponseMetadata
 import sttp.model.sse.ServerSentEvent
 import sttp.openai.OpenAI
 import sttp.openai.OpenAIExceptions.OpenAIException
 import sttp.openai.json.SttpUpickleApiExtension.deserializeJsonSnake
 import sttp.openai.requests.completions.chat.ChatChunkRequestResponseData.ChatChunkResponse
 import sttp.openai.requests.completions.chat.ChatRequestBody.ChatBody
-import sttp.client4.akkahttp.AkkaHttpServerSentEvents
 
 package object akka {
   import ChatChunkResponse.DoneEvent
@@ -27,25 +28,28 @@ package object akka {
       */
     def createStreamedChatCompletion(
         chatBody: ChatBody
-    ): StreamRequest[Either[OpenAIException, Source[ChatChunkResponse, Any]], AkkaStreams] =
-      client
+    ): StreamRequest[Either[OpenAIException, Source[ChatChunkResponse, Any]], AkkaStreams] = {
+      val request = client
         .createChatCompletionAsBinaryStream(AkkaStreams, chatBody)
-        .mapResponse(mapEventToResponse)
+
+      request.response(request.response.mapWithMetadata(mapEventToResponse))
+    }
   }
 
   private def mapEventToResponse(
-      response: Either[OpenAIException, Source[ByteString, Any]]
+      response: Either[OpenAIException, Source[ByteString, Any]],
+      metadata: ResponseMetadata
   ): Either[OpenAIException, Source[ChatChunkResponse, Any]] =
     response.map(
       _.via(AkkaHttpServerSentEvents.parse)
-        .via(deserializeEvent)
+        .via(deserializeEvent(metadata))
     )
 
-  private def deserializeEvent: Flow[ServerSentEvent, ChatChunkResponse, Any] =
+  private def deserializeEvent(metadata: ResponseMetadata): Flow[ServerSentEvent, ChatChunkResponse, Any] =
     Flow[ServerSentEvent]
       .takeWhile(_ != DoneEvent)
       .collect { case ServerSentEvent(Some(data), _, _, _) =>
-        deserializeJsonSnake[ChatChunkResponse].apply(data) match {
+        deserializeJsonSnake[ChatChunkResponse].apply(data, metadata) match {
           case Left(exception) => throw exception
           case Right(value)    => value
         }
diff --git a/streaming/fs2/src/main/scala/sttp/openai/streaming/fs2/package.scala b/streaming/fs2/src/main/scala/sttp/openai/streaming/fs2/package.scala
index bfd3707..d65579a 100644
--- a/streaming/fs2/src/main/scala/sttp/openai/streaming/fs2/package.scala
+++ b/streaming/fs2/src/main/scala/sttp/openai/streaming/fs2/package.scala
@@ -4,6 +4,7 @@ import _root_.fs2.{Pipe, RaiseThrowable, Stream}
 import sttp.capabilities.fs2.Fs2Streams
 import sttp.client4.StreamRequest
 import sttp.client4.impl.fs2.Fs2ServerSentEvents
+import sttp.model.ResponseMetadata
 import sttp.model.sse.ServerSentEvent
 import sttp.openai.OpenAI
 import sttp.openai.OpenAIExceptions.OpenAIException
@@ -26,24 +27,27 @@ package object fs2 {
       */
     def createStreamedChatCompletion[F[_]: RaiseThrowable](
         chatBody: ChatBody
-    ): StreamRequest[Either[OpenAIException, Stream[F, ChatChunkResponse]], Fs2Streams[F]] =
-      client
+    ): StreamRequest[Either[OpenAIException, Stream[F, ChatChunkResponse]], Fs2Streams[F]] = {
+      val request = client
         .createChatCompletionAsBinaryStream(Fs2Streams[F], chatBody)
-        .mapResponse(mapEventToResponse[F])
+
+      request.response(request.response.mapWithMetadata(mapEventToResponse[F]))
+    }
   }
 
   private def mapEventToResponse[F[_]: RaiseThrowable](
-      response: Either[OpenAIException, Stream[F, Byte]]
+      response: Either[OpenAIException, Stream[F, Byte]],
+      metadata: ResponseMetadata
   ): Either[OpenAIException, Stream[F, ChatChunkResponse]] =
     response.map(
       _.through(Fs2ServerSentEvents.parse)
-        .through(deserializeEvent)
+        .through(deserializeEvent(metadata))
         .rethrow
     )
 
-  private def deserializeEvent[F[_]]: Pipe[F, ServerSentEvent, Either[OpenAIException, ChatChunkResponse]] =
+  private def deserializeEvent[F[_]](metadata: ResponseMetadata): Pipe[F, ServerSentEvent, Either[Exception, ChatChunkResponse]] =
     _.takeWhile(_ != DoneEvent)
       .collect { case ServerSentEvent(Some(data), _, _, _) =>
-        deserializeJsonSnake[ChatChunkResponse].apply(data)
+        deserializeJsonSnake[ChatChunkResponse].apply(data, metadata)
       }
 }
diff --git a/streaming/ox/src/main/scala/sttp/openai/streaming/ox/oxStreaming.scala b/streaming/ox/src/main/scala/sttp/openai/streaming/ox/oxStreaming.scala
index aba9e94..f1b6946 100644
--- a/streaming/ox/src/main/scala/sttp/openai/streaming/ox/oxStreaming.scala
+++ b/streaming/ox/src/main/scala/sttp/openai/streaming/ox/oxStreaming.scala
@@ -1,18 +1,18 @@
 package sttp.openai.streaming.ox
 
+import ox.flow.Flow
 import sttp.client4.Request
 import sttp.client4.impl.ox.sse.OxServerSentEvents
+import sttp.model.ResponseMetadata
 import sttp.model.sse.ServerSentEvent
 import sttp.openai.OpenAI
 import sttp.openai.OpenAIExceptions.OpenAIException
-import sttp.openai.OpenAIExceptions.OpenAIException.DeserializationOpenAIException
 import sttp.openai.json.SttpUpickleApiExtension.deserializeJsonSnake
 import sttp.openai.requests.completions.chat.ChatChunkRequestResponseData.ChatChunkResponse
 import sttp.openai.requests.completions.chat.ChatChunkRequestResponseData.ChatChunkResponse.DoneEvent
 import sttp.openai.requests.completions.chat.ChatRequestBody.ChatBody
 
 import java.io.InputStream
-import ox.flow.Flow
 
 extension (client: OpenAI)
   /** Creates and streams a model response as chunk objects for the given chat conversation defined in chatBody.
@@ -26,19 +26,21 @@ extension (client: OpenAI)
     */
   def createStreamedChatCompletion(
       chatBody: ChatBody
-  ): Request[Either[OpenAIException, Flow[Either[DeserializationOpenAIException, ChatChunkResponse]]]] =
-    client
+  ): Request[Either[OpenAIException, Flow[Either[Exception, ChatChunkResponse]]]] =
+    val request = client
       .createChatCompletionAsInputStream(chatBody)
-      .mapResponse(mapEventToResponse)
+
+    request.response(request.response.mapWithMetadata(mapEventToResponse))
 
 private def mapEventToResponse(
-    response: Either[OpenAIException, InputStream]
-): Either[OpenAIException, Flow[Either[DeserializationOpenAIException, ChatChunkResponse]]] =
+    response: Either[OpenAIException, InputStream],
+    metadata: ResponseMetadata
+): Either[OpenAIException, Flow[Either[Exception, ChatChunkResponse]]] =
   response.map(s =>
     OxServerSentEvents
       .parse(s)
       .takeWhile(_ != DoneEvent)
       .collect { case ServerSentEvent(Some(data), _, _, _) =>
-        deserializeJsonSnake[ChatChunkResponse].apply(data)
+        deserializeJsonSnake[ChatChunkResponse].apply(data, metadata)
       }
   )
diff --git a/streaming/pekko/src/main/scala/sttp/openai/streaming/pekko/package.scala b/streaming/pekko/src/main/scala/sttp/openai/streaming/pekko/package.scala
index eeabd94..07cb958 100644
--- a/streaming/pekko/src/main/scala/sttp/openai/streaming/pekko/package.scala
+++ b/streaming/pekko/src/main/scala/sttp/openai/streaming/pekko/package.scala
@@ -4,13 +4,14 @@ import org.apache.pekko.stream.scaladsl.{Flow, Source}
 import org.apache.pekko.util.ByteString
 import sttp.capabilities.pekko.PekkoStreams
 import sttp.client4.StreamRequest
+import sttp.client4.pekkohttp.PekkoHttpServerSentEvents
+import sttp.model.ResponseMetadata
 import sttp.model.sse.ServerSentEvent
 import sttp.openai.OpenAI
 import sttp.openai.OpenAIExceptions.OpenAIException
 import sttp.openai.json.SttpUpickleApiExtension.deserializeJsonSnake
 import sttp.openai.requests.completions.chat.ChatChunkRequestResponseData.ChatChunkResponse
 import sttp.openai.requests.completions.chat.ChatRequestBody.ChatBody
-import sttp.client4.pekkohttp.PekkoHttpServerSentEvents
 
 package object pekko {
   import ChatChunkResponse.DoneEvent
@@ -27,25 +28,28 @@ package object pekko {
       */
     def createStreamedChatCompletion(
         chatBody: ChatBody
-    ): StreamRequest[Either[OpenAIException, Source[ChatChunkResponse, Any]], PekkoStreams] =
-      client
+    ): StreamRequest[Either[OpenAIException, Source[ChatChunkResponse, Any]], PekkoStreams] = {
+      val request = client
         .createChatCompletionAsBinaryStream(PekkoStreams, chatBody)
-        .mapResponse(mapEventToResponse)
+
+      request.response(request.response.mapWithMetadata(mapEventToResponse))
+    }
   }
 
   private def mapEventToResponse(
-      response: Either[OpenAIException, Source[ByteString, Any]]
+      response: Either[OpenAIException, Source[ByteString, Any]],
+      metadata: ResponseMetadata
   ): Either[OpenAIException, Source[ChatChunkResponse, Any]] =
     response.map(
       _.via(PekkoHttpServerSentEvents.parse)
-        .via(deserializeEvent)
+        .via(deserializeEvent(metadata))
     )
 
-  private def deserializeEvent: Flow[ServerSentEvent, ChatChunkResponse, Any] =
+  private def deserializeEvent(metadata: ResponseMetadata): Flow[ServerSentEvent, ChatChunkResponse, Any] =
     Flow[ServerSentEvent]
       .takeWhile(_ != DoneEvent)
       .collect { case ServerSentEvent(Some(data), _, _, _) =>
-        deserializeJsonSnake[ChatChunkResponse].apply(data) match {
+        deserializeJsonSnake[ChatChunkResponse].apply(data, metadata) match {
           case Left(exception) => throw exception
           case Right(value)    => value
         }
diff --git a/streaming/zio/src/main/scala/sttp/openai/streaming/zio/package.scala b/streaming/zio/src/main/scala/sttp/openai/streaming/zio/package.scala
index d94f8b9..557271d 100644
--- a/streaming/zio/src/main/scala/sttp/openai/streaming/zio/package.scala
+++ b/streaming/zio/src/main/scala/sttp/openai/streaming/zio/package.scala
@@ -1,16 +1,17 @@
 package sttp.openai.streaming
 
+import _root_.zio.ZIO
+import _root_.zio.stream._
+import sttp.capabilities.zio.ZioStreams
 import sttp.client4.StreamRequest
+import sttp.client4.impl.zio.ZioServerSentEvents
+import sttp.model.ResponseMetadata
 import sttp.model.sse.ServerSentEvent
 import sttp.openai.OpenAI
 import sttp.openai.OpenAIExceptions.OpenAIException
 import sttp.openai.json.SttpUpickleApiExtension.deserializeJsonSnake
 import sttp.openai.requests.completions.chat.ChatChunkRequestResponseData.ChatChunkResponse
 import sttp.openai.requests.completions.chat.ChatRequestBody.ChatBody
-import _root_.zio.stream._
-import _root_.zio.ZIO
-import sttp.capabilities.zio.ZioStreams
-import sttp.client4.impl.zio.ZioServerSentEvents
 
 package object zio {
   import ChatChunkResponse.DoneEvent
@@ -27,23 +28,25 @@ package object zio {
       */
     def createStreamedChatCompletion(
         chatBody: ChatBody
-    ): StreamRequest[Either[OpenAIException, Stream[Throwable, ChatChunkResponse]], ZioStreams] =
-      client
+    ): StreamRequest[Either[OpenAIException, Stream[Throwable, ChatChunkResponse]], ZioStreams] = {
+      val request = client
         .createChatCompletionAsBinaryStream(ZioStreams, chatBody)
-        .mapResponse(mapEventToResponse)
+      request.response(request.response.mapWithMetadata(mapEventToResponse))
+    }
   }
 
   private def mapEventToResponse(
-      response: Either[OpenAIException, Stream[Throwable, Byte]]
+      response: Either[OpenAIException, Stream[Throwable, Byte]],
+      metadata: ResponseMetadata
   ): Either[OpenAIException, Stream[Throwable, ChatChunkResponse]] =
     response.map(
       _.viaFunction(ZioServerSentEvents.parse)
-        .viaFunction(deserializeEvent)
+        .viaFunction(deserializeEvent(metadata))
     )
 
-  private def deserializeEvent: ZioStreams.Pipe[ServerSentEvent, ChatChunkResponse] =
+  private def deserializeEvent(metadata: ResponseMetadata): ZioStreams.Pipe[ServerSentEvent, ChatChunkResponse] =
     _.takeWhile(_ != DoneEvent)
       .collectZIO { case ServerSentEvent(Some(data), _, _, _) =>
-        ZIO.fromEither(deserializeJsonSnake[ChatChunkResponse].apply(data))
+        ZIO.fromEither(deserializeJsonSnake[ChatChunkResponse].apply(data, metadata))
       }
 }


Test patch:
diff --git a/core/src/test/scala/sttp/openai/client/SyncClientSpec.scala b/core/src/test/scala/sttp/openai/client/SyncClientSpec.scala
index eec6587..31dd2ad 100644
--- a/core/src/test/scala/sttp/openai/client/SyncClientSpec.scala
+++ b/core/src/test/scala/sttp/openai/client/SyncClientSpec.scala
@@ -4,20 +4,21 @@ import org.scalatest.EitherValues
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 import sttp.client4._
+import sttp.client4.testing.ResponseStub
+import sttp.model.StatusCode
 import sttp.model.StatusCode._
 import sttp.openai.OpenAIExceptions.OpenAIException
-import sttp.openai.OpenAISyncClient
+import sttp.openai.{CustomizeOpenAIRequest, OpenAISyncClient}
 import sttp.openai.fixtures.ErrorFixture
 import sttp.openai.requests.models.ModelsResponseData._
-import sttp.openai.CustomizeOpenAIRequest
+
 import java.util.concurrent.atomic.AtomicReference
-import sttp.client4.testing.ResponseStub
 
 class SyncClientSpec extends AnyFlatSpec with Matchers with EitherValues {
   for ((statusCode, expectedError) <- ErrorFixture.testData)
     s"Service response with status code: $statusCode" should s"return properly deserialized ${expectedError.getClass.getSimpleName}" in {
       // given
-      val syncBackendStub = DefaultSyncBackend.stub.whenAnyRequest.thenRespondWithCode(statusCode, ErrorFixture.errorResponse)
+      val syncBackendStub = DefaultSyncBackend.stub.whenAnyRequest.thenRespondAdjust(ErrorFixture.errorResponse, statusCode)
       val syncClient = OpenAISyncClient(authToken = "test-token", backend = syncBackendStub)
 
       // when
@@ -26,7 +27,7 @@ class SyncClientSpec extends AnyFlatSpec with Matchers with EitherValues {
       // then
       caught.getClass shouldBe expectedError.getClass: Unit
       caught.message shouldBe expectedError.message: Unit
-      caught.cause shouldBe expectedError.cause: Unit
+      caught.cause.getClass shouldBe expectedError.cause.getClass: Unit
       caught.code shouldBe expectedError.code: Unit
       caught.param shouldBe expectedError.param: Unit
       caught.`type` shouldBe expectedError.`type`
@@ -35,7 +36,7 @@ class SyncClientSpec extends AnyFlatSpec with Matchers with EitherValues {
   "Fetching models with successful response" should "return properly deserialized list of available models" in {
     // given
     val modelsResponse = sttp.openai.fixtures.ModelsGetResponse.singleModelResponse
-    val syncBackendStub = DefaultSyncBackend.stub.whenAnyRequest.thenRespondWithCode(Ok, modelsResponse)
+    val syncBackendStub = DefaultSyncBackend.stub.whenAnyRequest.thenRespondAdjust(modelsResponse, Ok)
     val syncClient = OpenAISyncClient(authToken = "test-token", backend = syncBackendStub)
     val deserializedModels = ModelsResponse(
       `object` = "list",
@@ -76,7 +77,7 @@ class SyncClientSpec extends AnyFlatSpec with Matchers with EitherValues {
     val capturedRequest = new AtomicReference[GenericRequest[_, _]](null)
     val syncBackendStub = DefaultSyncBackend.stub.whenAnyRequest.thenRespondF { request =>
       capturedRequest.set(request)
-      ResponseStub.ok(sttp.openai.fixtures.ModelsGetResponse.singleModelResponse)
+      ResponseStub.adjust(sttp.openai.fixtures.ModelsGetResponse.singleModelResponse, StatusCode.Ok)
     }
     val syncClient = OpenAISyncClient(authToken = "test-token", backend = syncBackendStub)
 
diff --git a/core/src/test/scala/sttp/openai/fixtures/ErrorFixture.scala b/core/src/test/scala/sttp/openai/fixtures/ErrorFixture.scala
index 298beab..31751f6 100644
--- a/core/src/test/scala/sttp/openai/fixtures/ErrorFixture.scala
+++ b/core/src/test/scala/sttp/openai/fixtures/ErrorFixture.scala
@@ -1,6 +1,7 @@
 package sttp.openai.fixtures
 
-import sttp.client4._
+import sttp.client4.ResponseException.UnexpectedStatusCode
+import sttp.client4.testing.ResponseStub
 import sttp.model.StatusCode
 import sttp.model.StatusCode._
 import sttp.openai.OpenAIExceptions.OpenAIException
@@ -23,39 +24,93 @@ object ErrorFixture {
   val testData: Seq[(StatusCode, OpenAIException)] = List(
     (
       TooManyRequests,
-      new RateLimitException(Some(message), Some(errorType), None, Some(code), HttpError(errorResponse, TooManyRequests))
+      new RateLimitException(
+        Some(message),
+        Some(errorType),
+        None,
+        Some(code),
+        UnexpectedStatusCode(errorResponse, ResponseStub.adjust(errorResponse, TooManyRequests))
+      )
     ),
     (
       BadRequest,
-      new InvalidRequestException(Some(message), Some(errorType), None, Some(code), HttpError(errorResponse, BadRequest))
+      new InvalidRequestException(
+        Some(message),
+        Some(errorType),
+        None,
+        Some(code),
+        UnexpectedStatusCode(errorResponse, ResponseStub.adjust(errorResponse, BadRequest))
+      )
     ),
     (
       NotFound,
-      new InvalidRequestException(Some(message), Some(errorType), None, Some(code), HttpError(errorResponse, NotFound))
+      new InvalidRequestException(
+        Some(message),
+        Some(errorType),
+        None,
+        Some(code),
+        UnexpectedStatusCode(errorResponse, ResponseStub.adjust(errorResponse, NotFound))
+      )
     ),
     (
       UnsupportedMediaType,
-      new InvalidRequestException(Some(message), Some(errorType), None, Some(code), HttpError(errorResponse, UnsupportedMediaType))
+      new InvalidRequestException(
+        Some(message),
+        Some(errorType),
+        None,
+        Some(code),
+        UnexpectedStatusCode(errorResponse, ResponseStub.adjust(errorResponse, UnsupportedMediaType))
+      )
     ),
     (
       Unauthorized,
-      new AuthenticationException(Some(message), Some(errorType), None, Some(code), HttpError(errorResponse, Unauthorized))
+      new AuthenticationException(
+        Some(message),
+        Some(errorType),
+        None,
+        Some(code),
+        UnexpectedStatusCode(errorResponse, ResponseStub.adjust(errorResponse, Unauthorized))
+      )
     ),
     (
       Forbidden,
-      new PermissionException(Some(message), Some(errorType), None, Some(code), HttpError(errorResponse, Forbidden))
+      new PermissionException(
+        Some(message),
+        Some(errorType),
+        None,
+        Some(code),
+        UnexpectedStatusCode(errorResponse, ResponseStub.adjust(errorResponse, Forbidden))
+      )
     ),
     (
       Conflict,
-      new TryAgain(Some(message), Some(errorType), None, Some(code), HttpError(errorResponse, Conflict))
+      new TryAgain(
+        Some(message),
+        Some(errorType),
+        None,
+        Some(code),
+        UnexpectedStatusCode(errorResponse, ResponseStub.adjust(errorResponse, Conflict))
+      )
     ),
     (
       ServiceUnavailable,
-      new ServiceUnavailableException(Some(message), Some(errorType), None, Some(code), HttpError(errorResponse, ServiceUnavailable))
+      new ServiceUnavailableException(
+        Some(message),
+        Some(errorType),
+        None,
+        Some(code),
+        UnexpectedStatusCode(errorResponse, ResponseStub.adjust(errorResponse, ServiceUnavailable))
+      )
     ),
     (
       Gone,
-      new APIException(Some(message), Some(errorType), None, Some(code), HttpError(errorResponse, Gone))
+      new APIException(
+        Some(message),
+        Some(errorType),
+        None,
+        Some(code),
+        UnexpectedStatusCode(errorResponse, ResponseStub.adjust(errorResponse, Gone))
+      )
     )
   )
 }
diff --git a/core/src/test/scala/sttp/openai/requests/BatchDataSpec.scala b/core/src/test/scala/sttp/openai/requests/BatchDataSpec.scala
index 2ce3aec..0c933e9 100644
--- a/core/src/test/scala/sttp/openai/requests/BatchDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/BatchDataSpec.scala
@@ -4,8 +4,9 @@ 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.json.SnakePickle
 import sttp.openai.requests.batch.{BatchRequestBody, BatchResponse, ListBatchResponse}
+import sttp.openai.utils.JsonUtils
 
 class BatchDataSpec extends AnyFlatSpec with Matchers with EitherValues {
 
@@ -30,7 +31,7 @@ class BatchDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     val expectedResponse: BatchResponse = BatchFixture.batchResponse
     // when
     val deserializedJsonResponse: Either[Exception, BatchResponse] =
-      SttpUpickleApiExtension.deserializeJsonSnake[BatchResponse].apply(jsonResponse)
+      JsonUtils.deserializeJsonSnake[BatchResponse].apply(jsonResponse)
     // then
     deserializedJsonResponse.value shouldBe expectedResponse
   }
@@ -46,7 +47,7 @@ class BatchDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
     // when
     val deserializedJsonResponse: Either[Exception, ListBatchResponse] =
-      SttpUpickleApiExtension.deserializeJsonSnake[ListBatchResponse].apply(jsonResponse)
+      JsonUtils.deserializeJsonSnake[ListBatchResponse].apply(jsonResponse)
 
     // then
     deserializedJsonResponse.value shouldBe expectedResponse
diff --git a/core/src/test/scala/sttp/openai/requests/admin/AdminApiKeyDataSpec.scala b/core/src/test/scala/sttp/openai/requests/admin/AdminApiKeyDataSpec.scala
index f77a1fc..6ea3c81 100644
--- a/core/src/test/scala/sttp/openai/requests/admin/AdminApiKeyDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/admin/AdminApiKeyDataSpec.scala
@@ -4,7 +4,8 @@ import org.scalatest.EitherValues
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 import sttp.openai.fixtures.AdminFixture
-import sttp.openai.json.{SnakePickle, SttpUpickleApiExtension}
+import sttp.openai.json.SnakePickle
+import sttp.openai.utils.JsonUtils
 
 class AdminApiKeyDataSpec extends AnyFlatSpec with Matchers with EitherValues {
   "Given create admin api key request as case class" should "be properly serialized to Json" in {
@@ -25,7 +26,7 @@ class AdminApiKeyDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     val expectedResponse: AdminApiKeyResponse = AdminFixture.adminApiKeyResponse
     // when
     val deserializedJsonResponse: Either[Exception, AdminApiKeyResponse] =
-      SttpUpickleApiExtension.deserializeJsonSnake[AdminApiKeyResponse].apply(jsonResponse)
+      JsonUtils.deserializeJsonSnake[AdminApiKeyResponse].apply(jsonResponse)
     // then
     deserializedJsonResponse.value shouldBe expectedResponse
   }
@@ -41,7 +42,7 @@ class AdminApiKeyDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
     // when
     val deserializedJsonResponse: Either[Exception, ListAdminApiKeyResponse] =
-      SttpUpickleApiExtension.deserializeJsonSnake[ListAdminApiKeyResponse].apply(jsonResponse)
+      JsonUtils.deserializeJsonSnake[ListAdminApiKeyResponse].apply(jsonResponse)
     // then
     deserializedJsonResponse.value shouldBe expectedResponse
   }
@@ -55,7 +56,7 @@ class AdminApiKeyDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
     // when
     val deserializedJsonResponse: Either[Exception, DeleteAdminApiKeyResponse] =
-      SttpUpickleApiExtension.deserializeJsonSnake[DeleteAdminApiKeyResponse].apply(jsonResponse)
+      JsonUtils.deserializeJsonSnake[DeleteAdminApiKeyResponse].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 32b9552..f72c7c4 100644
--- a/core/src/test/scala/sttp/openai/requests/assistants/AssistantsDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/assistants/AssistantsDataSpec.scala
@@ -6,11 +6,10 @@ import org.scalatest.matchers.should.Matchers
 import sttp.client4.IsOption._
 import sttp.openai.fixtures
 import sttp.openai.json.SnakePickle
-import sttp.openai.json.SttpUpickleApiExtension
-import sttp.openai.requests.completions.chat.message.Tool.CodeInterpreterTool
-import sttp.openai.requests.completions.chat.message.Tool.FileSearchTool
+import sttp.openai.requests.completions.chat.message.Tool.{CodeInterpreterTool, FileSearchTool}
 import sttp.openai.requests.completions.chat.message.ToolResource.FileSearchToolResource
 import sttp.openai.requests.completions.chat.message.ToolResources
+import sttp.openai.utils.JsonUtils
 
 class AssistantsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
 
@@ -57,7 +56,7 @@ class AssistantsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
 
     // when
-    val givenResponse: Either[Exception, AssistantData] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, AssistantData] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     val json = givenResponse.value
@@ -116,7 +115,7 @@ class AssistantsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
 
     // when
-    val givenResponse: Either[Exception, ListAssistantsResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, ListAssistantsResponse] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
@@ -174,7 +173,7 @@ class AssistantsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
 
     // when
-    val givenResponse: Either[Exception, ListAssistantsResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, ListAssistantsResponse] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
@@ -202,7 +201,7 @@ class AssistantsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
 
     // when
-    val givenResponse: Either[Exception, AssistantData] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, AssistantData] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
@@ -253,7 +252,7 @@ class AssistantsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
 
     // when
-    val givenResponse: Either[Exception, AssistantData] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, AssistantData] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
@@ -272,7 +271,7 @@ class AssistantsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
 
     // when
-    val givenResponse: Either[Exception, DeleteAssistantResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, DeleteAssistantResponse] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
diff --git a/core/src/test/scala/sttp/openai/requests/audio/AudioCreationDataSpec.scala b/core/src/test/scala/sttp/openai/requests/audio/AudioCreationDataSpec.scala
index 556eab0..40f04c6 100644
--- a/core/src/test/scala/sttp/openai/requests/audio/AudioCreationDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/audio/AudioCreationDataSpec.scala
@@ -4,7 +4,7 @@ import org.scalatest.EitherValues
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 import sttp.openai.fixtures
-import sttp.openai.json.SttpUpickleApiExtension
+import sttp.openai.utils.JsonUtils
 
 class AudioCreationDataSpec extends AnyFlatSpec with Matchers with EitherValues {
   "Given audio generation response as Json" should "be properly deserialized to case class" in {
@@ -19,7 +19,7 @@ class AudioCreationDataSpec extends AnyFlatSpec with Matchers with EitherValues
     )
 
     // when
-    val deserializedJsonResponse = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val deserializedJsonResponse = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     deserializedJsonResponse.value shouldBe expectedResponse
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 0601a9a..81b87db 100644
--- a/core/src/test/scala/sttp/openai/requests/completions/CompletionsDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/completions/CompletionsDataSpec.scala
@@ -4,14 +4,15 @@ import org.scalatest.EitherValues
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 import sttp.openai.fixtures
-import sttp.openai.json.{SnakePickle, SttpUpickleApiExtension}
+import sttp.openai.json.SnakePickle
 import sttp.openai.requests.completions.Stop.SingleStop
+import sttp.openai.utils.JsonUtils
 
 class CompletionsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
   "Given completions response as Json" should "be properly deserialized to case class" in {
-    import sttp.openai.requests.completions.CompletionsResponseData._
-    import sttp.openai.requests.completions.CompletionsResponseData.CompletionsResponse._
     import sttp.openai.requests.completions.CompletionsRequestBody.CompletionModel.GPT35TurboInstruct
+    import sttp.openai.requests.completions.CompletionsResponseData.CompletionsResponse._
+    import sttp.openai.requests.completions.CompletionsResponseData._
 
     // given
     val jsonResponse = fixtures.CompletionsFixture.jsonSinglePromptResponse
@@ -43,16 +44,16 @@ class CompletionsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
 
     // when
-    val givenResponse: Either[Exception, CompletionsResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, CompletionsResponse] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
   }
 
   "Given completions request as case class" should "be properly serialized to Json" in {
-    import sttp.openai.requests.completions.CompletionsRequestBody._
-    import sttp.openai.requests.completions.CompletionsRequestBody.CompletionsBody._
     import sttp.openai.requests.completions.CompletionsRequestBody.CompletionModel.GPT35TurboInstruct
+    import sttp.openai.requests.completions.CompletionsRequestBody.CompletionsBody._
+    import sttp.openai.requests.completions.CompletionsRequestBody._
 
     // given
     val givenRequest = CompletionsRequestBody.CompletionsBody(
@@ -76,9 +77,9 @@ class CompletionsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
   }
 
   "Given completions of MultiplePrompt response as Json" should "be properly deserialized to case class" in {
-    import sttp.openai.requests.completions.CompletionsResponseData._
-    import sttp.openai.requests.completions.CompletionsResponseData.CompletionsResponse._
     import sttp.openai.requests.completions.CompletionsRequestBody.CompletionModel.GPT35TurboInstruct
+    import sttp.openai.requests.completions.CompletionsResponseData.CompletionsResponse._
+    import sttp.openai.requests.completions.CompletionsResponseData._
 
     // given
     val jsonResponse = fixtures.CompletionsFixture.jsonMultiplePromptResponse
@@ -116,16 +117,16 @@ class CompletionsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
 
     // when
-    val givenResponse: Either[Exception, CompletionsResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, CompletionsResponse] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
   }
 
   "Given completions of MultiplePrompt request as case class" should "be properly serialized to Json" in {
-    import sttp.openai.requests.completions.CompletionsRequestBody._
-    import sttp.openai.requests.completions.CompletionsRequestBody.CompletionsBody._
     import sttp.openai.requests.completions.CompletionsRequestBody.CompletionModel.GPT35TurboInstruct
+    import sttp.openai.requests.completions.CompletionsRequestBody.CompletionsBody._
+    import sttp.openai.requests.completions.CompletionsRequestBody._
 
     // given
     val givenRequest = CompletionsRequestBody.CompletionsBody(
diff --git a/core/src/test/scala/sttp/openai/requests/completions/chat/ChatChunkDataSpec.scala b/core/src/test/scala/sttp/openai/requests/completions/chat/ChatChunkDataSpec.scala
index a24981c..822517d 100644
--- a/core/src/test/scala/sttp/openai/requests/completions/chat/ChatChunkDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/completions/chat/ChatChunkDataSpec.scala
@@ -4,9 +4,9 @@ import org.scalatest.EitherValues
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 import sttp.openai.fixtures
-import sttp.openai.json.SttpUpickleApiExtension
 import sttp.openai.requests.completions.Stop.SingleStop
 import sttp.openai.utils.ChatCompletionFixtures._
+import sttp.openai.utils.JsonUtils
 
 class ChatChunkDataSpec extends AnyFlatSpec with Matchers with EitherValues {
 
@@ -39,7 +39,7 @@ class ChatChunkDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
 
     // when
-    val givenResponse: Either[Exception, ChatChunkResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, ChatChunkResponse] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
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 6ec0a0d..88fcf4b 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,13 +4,14 @@ import org.scalatest.EitherValues
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 import sttp.openai.fixtures
-import sttp.openai.json.{SnakePickle, SttpUpickleApiExtension}
+import sttp.openai.json.SnakePickle
 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._
+import sttp.openai.utils.JsonUtils
 
 class ChatDataSpec extends AnyFlatSpec with Matchers with EitherValues {
 
@@ -25,7 +26,7 @@ class ChatDataSpec extends AnyFlatSpec with Matchers with EitherValues {
       deleted = true
     )
     // when
-    val givenResponse: Either[Exception, DeleteChatCompletionResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, DeleteChatCompletionResponse] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
     // then
     givenResponse.value shouldBe expectedResponse
   }
@@ -43,7 +44,7 @@ class ChatDataSpec extends AnyFlatSpec with Matchers with EitherValues {
       hasMore = false
     )
     // when
-    val givenResponse: Either[Exception, ListChatResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, ListChatResponse] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
     // then
     givenResponse.value shouldBe expectedResponse
   }
@@ -75,7 +76,7 @@ class ChatDataSpec extends AnyFlatSpec with Matchers with EitherValues {
       hasMore = true
     )
     // when
-    val givenResponse: Either[Exception, ListMessageResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, ListMessageResponse] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
     // then
     givenResponse.value shouldBe expectedResponse
   }
@@ -149,7 +150,7 @@ class ChatDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
 
     // when
-    val givenResponse: Either[Exception, ChatResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, ChatResponse] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
diff --git a/core/src/test/scala/sttp/openai/requests/embeddings/EmbeddingsDataSpec.scala b/core/src/test/scala/sttp/openai/requests/embeddings/EmbeddingsDataSpec.scala
index c18a514..d6caaf2 100644
--- a/core/src/test/scala/sttp/openai/requests/embeddings/EmbeddingsDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/embeddings/EmbeddingsDataSpec.scala
@@ -4,9 +4,9 @@ import org.scalatest.EitherValues
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 import sttp.openai.fixtures
-import sttp.openai.json.SttpUpickleApiExtension
-import EmbeddingsResponseBody._
 import sttp.openai.requests.embeddings.EmbeddingsRequestBody.EmbeddingsModel
+import sttp.openai.requests.embeddings.EmbeddingsResponseBody._
+import sttp.openai.utils.JsonUtils
 
 class EmbeddingsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
   "Given list files response as Json" should "be properly deserialized to case class" in {
@@ -31,7 +31,7 @@ class EmbeddingsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
     // when
     val givenResponse: Either[Exception, EmbeddingResponse] =
-      SttpUpickleApiExtension.deserializeJsonSnake[EmbeddingResponse].apply(listFilesResponse)
+      JsonUtils.deserializeJsonSnake[EmbeddingResponse].apply(listFilesResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
diff --git a/core/src/test/scala/sttp/openai/requests/files/FilesResponseDataSpec.scala b/core/src/test/scala/sttp/openai/requests/files/FilesResponseDataSpec.scala
index 1f9aa36..0cfd5ea 100644
--- a/core/src/test/scala/sttp/openai/requests/files/FilesResponseDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/files/FilesResponseDataSpec.scala
@@ -4,9 +4,9 @@ import org.scalatest.EitherValues
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 import sttp.openai.fixtures
-import sttp.openai.json.SttpUpickleApiExtension
-import sttp.openai.requests.files.FilesResponseData.{DeletedFileData, FileData, FilesResponse}
 import sttp.openai.requests.files.FilesResponseData.FilesResponse._
+import sttp.openai.requests.files.FilesResponseData.{DeletedFileData, FileData, FilesResponse}
+import sttp.openai.utils.JsonUtils
 
 class FilesResponseDataSpec extends AnyFlatSpec with Matchers with EitherValues {
   "Given list files response as Json" should "be properly deserialized to case class" in {
@@ -29,7 +29,7 @@ class FilesResponseDataSpec extends AnyFlatSpec with Matchers with EitherValues
     )
 
     // when
-    val givenResponse: Either[Exception, FilesResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(listFilesResponse)
+    val givenResponse: Either[Exception, FilesResponse] = JsonUtils.deserializeJsonSnake.apply(listFilesResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
@@ -51,7 +51,7 @@ class FilesResponseDataSpec extends AnyFlatSpec with Matchers with EitherValues
       )
 
     // when
-    val givenResponse: Either[Exception, FileData] = SttpUpickleApiExtension.deserializeJsonSnake[FileData].apply(singleFileJsonResponse)
+    val givenResponse: Either[Exception, FileData] = JsonUtils.deserializeJsonSnake[FileData].apply(singleFileJsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
@@ -68,7 +68,7 @@ class FilesResponseDataSpec extends AnyFlatSpec with Matchers with EitherValues
 
     // when
     val givenResponse: Either[Exception, DeletedFileData] =
-      SttpUpickleApiExtension.deserializeJsonSnake[DeletedFileData].apply(listFilesResponse)
+      JsonUtils.deserializeJsonSnake[DeletedFileData].apply(listFilesResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
@@ -89,7 +89,7 @@ class FilesResponseDataSpec extends AnyFlatSpec with Matchers with EitherValues
     )
 
     // when
-    val givenResponse: Either[Exception, FileData] = SttpUpickleApiExtension.deserializeJsonSnake[FileData].apply(retrieveFileJsonResponse)
+    val givenResponse: Either[Exception, FileData] = JsonUtils.deserializeJsonSnake[FileData].apply(retrieveFileJsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
diff --git a/core/src/test/scala/sttp/openai/requests/finetuning/FineTuningDataSpec.scala b/core/src/test/scala/sttp/openai/requests/finetuning/FineTuningDataSpec.scala
index c1ca45a..8e73529 100644
--- a/core/src/test/scala/sttp/openai/requests/finetuning/FineTuningDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/finetuning/FineTuningDataSpec.scala
@@ -4,8 +4,9 @@ import org.scalatest.EitherValues
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 import sttp.openai.fixtures.FineTuningJobFixture
-import sttp.openai.json.{SnakePickle, SttpUpickleApiExtension}
+import sttp.openai.json.SnakePickle
 import sttp.openai.requests.finetuning.FineTuningModel.GPT35Turbo0125
+import sttp.openai.utils.JsonUtils
 import ujson.Str
 
 class FineTuningDataSpec extends AnyFlatSpec with Matchers with EitherValues {
@@ -73,7 +74,7 @@ class FineTuningDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     val expectedResponse: FineTuningJobResponse = FineTuningJobFixture.fineTuningJobResponse
     // when
     val deserializedJsonResponse: Either[Exception, FineTuningJobResponse] =
-      SttpUpickleApiExtension.deserializeJsonSnake[FineTuningJobResponse].apply(jsonResponse)
+      JsonUtils.deserializeJsonSnake[FineTuningJobResponse].apply(jsonResponse)
     // then
     deserializedJsonResponse.value shouldBe expectedResponse
   }
@@ -87,7 +88,7 @@ class FineTuningDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
     // when
     val deserializedJsonResponse: Either[Exception, ListFineTuningJobResponse] =
-      SttpUpickleApiExtension.deserializeJsonSnake[ListFineTuningJobResponse].apply(jsonResponse)
+      JsonUtils.deserializeJsonSnake[ListFineTuningJobResponse].apply(jsonResponse)
 
     // then
     deserializedJsonResponse.value shouldBe expectedResponse
@@ -130,7 +131,7 @@ class FineTuningDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
     // when
     val deserializedJsonResponse: Either[Exception, ListFineTuningJobEventResponse] =
-      SttpUpickleApiExtension.deserializeJsonSnake[ListFineTuningJobEventResponse].apply(jsonResponse)
+      JsonUtils.deserializeJsonSnake[ListFineTuningJobEventResponse].apply(jsonResponse)
     // then
     deserializedJsonResponse.value shouldBe expectedResponse
   }
@@ -163,7 +164,7 @@ class FineTuningDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
     // when
     val deserializedJsonResponse: Either[Exception, ListFineTuningJobCheckpointResponse] =
-      SttpUpickleApiExtension.deserializeJsonSnake[ListFineTuningJobCheckpointResponse].apply(jsonResponse)
+      JsonUtils.deserializeJsonSnake[ListFineTuningJobCheckpointResponse].apply(jsonResponse)
     // then
     deserializedJsonResponse.value shouldBe expectedResponse
   }
diff --git a/core/src/test/scala/sttp/openai/requests/images/creation/ImageCreationDataSpec.scala b/core/src/test/scala/sttp/openai/requests/images/creation/ImageCreationDataSpec.scala
index 36287a6..f590ce4 100644
--- a/core/src/test/scala/sttp/openai/requests/images/creation/ImageCreationDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/images/creation/ImageCreationDataSpec.scala
@@ -4,13 +4,14 @@ import org.scalatest.EitherValues
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 import sttp.openai.fixtures
-import sttp.openai.json.{SnakePickle, SttpUpickleApiExtension}
+import sttp.openai.json.SnakePickle
 import sttp.openai.requests.images.{ResponseFormat, Size}
+import sttp.openai.utils.JsonUtils
 class ImageCreationDataSpec extends AnyFlatSpec with Matchers with EitherValues {
 
   "Given image generation response as Json" should "be properly deserialized to case class" in {
-    import sttp.openai.requests.images.ImageResponseData._
     import sttp.openai.requests.images.ImageResponseData.ImageResponse._
+    import sttp.openai.requests.images.ImageResponseData._
 
     // given
     val jsonResponse = fixtures.ImageCreationFixture.jsonResponse
@@ -24,15 +25,15 @@ class ImageCreationDataSpec extends AnyFlatSpec with Matchers with EitherValues
       data = generatedImageData
     )
     // when
-    val givenResponse = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
   }
 
   "Given create image request as case class" should "be properly serialized to Json" in {
-    import sttp.openai.requests.images.creation.ImageCreationRequestBody._
     import sttp.openai.requests.images.creation.ImageCreationRequestBody.ImageCreationBody._
+    import sttp.openai.requests.images.creation.ImageCreationRequestBody._
 
     // given
     val givenRequest: ImageCreationBody = ImageCreationBody(
diff --git a/core/src/test/scala/sttp/openai/requests/models/ModelsGetResponseDataSpec.scala b/core/src/test/scala/sttp/openai/requests/models/ModelsGetResponseDataSpec.scala
index 36cbe4b..7e1c49c 100644
--- a/core/src/test/scala/sttp/openai/requests/models/ModelsGetResponseDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/models/ModelsGetResponseDataSpec.scala
@@ -3,10 +3,10 @@ package sttp.openai.requests.models
 import org.scalatest.EitherValues
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
-import ModelsResponseData.{ModelData, ModelPermission, ModelsResponse}
-import ModelsResponseData.ModelsResponse._
 import sttp.openai.fixtures
-import sttp.openai.json.SttpUpickleApiExtension
+import sttp.openai.requests.models.ModelsResponseData.ModelsResponse._
+import sttp.openai.requests.models.ModelsResponseData.{ModelData, ModelPermission, ModelsResponse}
+import sttp.openai.utils.JsonUtils
 
 class ModelsGetResponseDataSpec extends AnyFlatSpec with Matchers with EitherValues {
 
@@ -74,7 +74,7 @@ class ModelsGetResponseDataSpec extends AnyFlatSpec with Matchers with EitherVal
     val expectedResponse: ModelsResponse = ModelsResponse(`object` = "list", data = serializedData)
     // when
 
-    val givenResponse: Either[Exception, ModelsResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(response)
+    val givenResponse: Either[Exception, ModelsResponse] = JsonUtils.deserializeJsonSnake.apply(response)
 
     // then
     givenResponse.value shouldBe expectedResponse
diff --git a/core/src/test/scala/sttp/openai/requests/moderations/ModerationsDataSpec.scala b/core/src/test/scala/sttp/openai/requests/moderations/ModerationsDataSpec.scala
index d1290e7..2a0b447 100644
--- a/core/src/test/scala/sttp/openai/requests/moderations/ModerationsDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/moderations/ModerationsDataSpec.scala
@@ -4,9 +4,9 @@ import org.scalatest.EitherValues
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 import sttp.openai.fixtures
-import sttp.openai.json.SttpUpickleApiExtension
 import sttp.openai.requests.moderations.ModerationsRequestBody.ModerationModel
 import sttp.openai.requests.moderations.ModerationsResponseData._
+import sttp.openai.utils.JsonUtils
 
 class ModerationsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
   "Given create moderation response as Json" should "be properly deserialized to case class" in {
@@ -41,7 +41,7 @@ class ModerationsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
     // when
     val givenResponse: Either[Exception, ModerationData] =
-      SttpUpickleApiExtension.deserializeJsonSnake[ModerationData].apply(createModerationResponse)
+      JsonUtils.deserializeJsonSnake[ModerationData].apply(createModerationResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
@@ -51,7 +51,7 @@ class ModerationsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     val createModerationResponse = fixtures.ModerationsFixture.jsonCreateModerationResponseUnknownModel
     // when
     val givenResponse: Either[Exception, ModerationData] =
-      SttpUpickleApiExtension.deserializeJsonSnake[ModerationData].apply(createModerationResponse)
+      JsonUtils.deserializeJsonSnake[ModerationData].apply(createModerationResponse)
 
     // then
     givenResponse.value.model shouldBe ModerationModel.CustomModerationModel("text-moderation-007")
diff --git a/core/src/test/scala/sttp/openai/requests/threads/ThreadsDataSpec.scala b/core/src/test/scala/sttp/openai/requests/threads/ThreadsDataSpec.scala
index 0c6d51d..d07bb1a 100644
--- a/core/src/test/scala/sttp/openai/requests/threads/ThreadsDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/threads/ThreadsDataSpec.scala
@@ -6,10 +6,10 @@ import org.scalatest.matchers.should.Matchers
 import sttp.client4.IsOption._
 import sttp.openai.fixtures
 import sttp.openai.json.SnakePickle
-import sttp.openai.json.SttpUpickleApiExtension
 import sttp.openai.requests.completions.chat.message.Attachment
 import sttp.openai.requests.completions.chat.message.Tool.{CodeInterpreterTool, FileSearchTool}
 import sttp.openai.requests.threads.messages.ThreadMessagesRequestBody.CreateMessage
+import sttp.openai.utils.JsonUtils
 class ThreadsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
 
   "Given empty create thread request as case class" should "be properly serialized to Json" in {
@@ -123,7 +123,7 @@ class ThreadsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
 
     // when
-    val givenResponse: Either[Exception, ThreadData] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, ThreadData] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
@@ -143,7 +143,7 @@ class ThreadsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
 
     // when
-    val givenResponse: Either[Exception, ThreadData] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, ThreadData] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
@@ -162,7 +162,7 @@ class ThreadsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
 
     // when
-    val givenResponse: Either[Exception, DeleteThreadResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, DeleteThreadResponse] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
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 57010a0..df73cae 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
@@ -4,10 +4,11 @@ import org.scalatest.EitherValues
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 import sttp.openai.fixtures
-import sttp.openai.json.{SnakePickle, SttpUpickleApiExtension}
+import sttp.openai.json.SnakePickle
 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.{DeleteMessageResponse, ListMessagesResponse, MessageData}
+import sttp.openai.utils.JsonUtils
 
 class ThreadMessagesDataSpec extends AnyFlatSpec with Matchers with EitherValues {
 
@@ -55,7 +56,7 @@ class ThreadMessagesDataSpec extends AnyFlatSpec with Matchers with EitherValues
     )
 
     // when
-    val givenResponse: Either[Exception, MessageData] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, MessageData] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
@@ -115,7 +116,7 @@ class ThreadMessagesDataSpec extends AnyFlatSpec with Matchers with EitherValues
     )
 
     // when
-    val givenResponse: Either[Exception, ListMessagesResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, ListMessagesResponse] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
@@ -147,7 +148,7 @@ class ThreadMessagesDataSpec extends AnyFlatSpec with Matchers with EitherValues
     )
 
     // when
-    val givenResponse: Either[Exception, MessageData] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, MessageData] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
@@ -182,7 +183,7 @@ class ThreadMessagesDataSpec extends AnyFlatSpec with Matchers with EitherValues
     )
 
     // when
-    val givenResponse: Either[Exception, MessageData] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, MessageData] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
@@ -198,7 +199,7 @@ class ThreadMessagesDataSpec extends AnyFlatSpec with Matchers with EitherValues
     )
 
     // when
-    val givenResponse: Either[Exception, DeleteMessageResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, DeleteMessageResponse] = JsonUtils.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 cc1f84e..5a0d97a 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
@@ -4,7 +4,7 @@ import org.scalatest.EitherValues
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 import sttp.openai.fixtures
-import sttp.openai.json.{SnakePickle, SttpUpickleApiExtension}
+import sttp.openai.json.SnakePickle
 import sttp.openai.requests.completions.chat.message.Tool.{CodeInterpreterTool, FileSearchTool, FunctionTool}
 import sttp.openai.requests.completions.chat.message.ToolResource.CodeInterpreterToolResource
 import sttp.openai.requests.completions.chat.message.ToolResources
@@ -18,6 +18,7 @@ import sttp.openai.requests.threads.runs.ThreadRunsResponseData.{
   RunStepData,
   Usage
 }
+import sttp.openai.utils.JsonUtils
 import ujson.{Arr, Obj, Str}
 
 class ThreadRunsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
@@ -65,7 +66,7 @@ class ThreadRunsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
 
     // when
-    val givenResponse: Either[Exception, RunData] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, RunData] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
@@ -117,7 +118,7 @@ class ThreadRunsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
 
     // when
-    val givenResponse: Either[Exception, RunData] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, RunData] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
@@ -186,7 +187,7 @@ class ThreadRunsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
       )
 
     // when
-    val givenResponse: Either[Exception, ListRunsResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, ListRunsResponse] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
@@ -233,7 +234,7 @@ class ThreadRunsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
       )
 
     // when
-    val givenResponse: Either[Exception, ListRunStepsResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, ListRunStepsResponse] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
@@ -268,7 +269,7 @@ class ThreadRunsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
       )
 
     // when
-    val givenResponse: Either[Exception, RunData] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, RunData] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
@@ -300,7 +301,7 @@ class ThreadRunsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
       )
 
     // when
-    val givenResponse: Either[Exception, RunStepData] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, RunStepData] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
@@ -350,7 +351,7 @@ class ThreadRunsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
       )
 
     // when
-    val givenResponse: Either[Exception, RunData] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, RunData] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
@@ -424,7 +425,7 @@ class ThreadRunsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
       )
 
     // when
-    val givenResponse: Either[Exception, RunData] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, RunData] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
@@ -459,7 +460,7 @@ class ThreadRunsDataSpec extends AnyFlatSpec with Matchers with EitherValues {
       )
 
     // when
-    val givenResponse: Either[Exception, RunData] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val givenResponse: Either[Exception, RunData] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     givenResponse.value shouldBe expectedResponse
diff --git a/core/src/test/scala/sttp/openai/requests/upload/UploadDataSpec.scala b/core/src/test/scala/sttp/openai/requests/upload/UploadDataSpec.scala
index 41ec2ff..07960b8 100644
--- a/core/src/test/scala/sttp/openai/requests/upload/UploadDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/upload/UploadDataSpec.scala
@@ -4,7 +4,8 @@ import org.scalatest.EitherValues
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 import sttp.openai.fixtures.UploadFixture
-import sttp.openai.json.{SnakePickle, SttpUpickleApiExtension}
+import sttp.openai.json.SnakePickle
+import sttp.openai.utils.JsonUtils
 
 class UploadDataSpec extends AnyFlatSpec with Matchers with EitherValues {
 
@@ -60,7 +61,7 @@ class UploadDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
     // when
     val deserializedJsonResponse: Either[Exception, UploadResponse] =
-      SttpUpickleApiExtension.deserializeJsonSnake[UploadResponse].apply(jsonResponse)
+      JsonUtils.deserializeJsonSnake[UploadResponse].apply(jsonResponse)
     // then
     deserializedJsonResponse.value shouldBe expectedResponse
   }
@@ -75,7 +76,7 @@ class UploadDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
     // when
     val deserializedJsonResponse: Either[Exception, UploadPartResponse] =
-      SttpUpickleApiExtension.deserializeJsonSnake[UploadPartResponse].apply(jsonResponse)
+      JsonUtils.deserializeJsonSnake[UploadPartResponse].apply(jsonResponse)
     // then
     deserializedJsonResponse.value shouldBe expectedResponse
   }
diff --git a/core/src/test/scala/sttp/openai/requests/vectorstore/VectorStoreDataSpec.scala b/core/src/test/scala/sttp/openai/requests/vectorstore/VectorStoreDataSpec.scala
index be83cbb..d89ae38 100644
--- a/core/src/test/scala/sttp/openai/requests/vectorstore/VectorStoreDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/vectorstore/VectorStoreDataSpec.scala
@@ -4,16 +4,10 @@ import org.scalatest.EitherValues
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 import sttp.openai.fixtures.VectorStoreFixture
-import sttp.openai.json.{SnakePickle, SttpUpickleApiExtension}
+import sttp.openai.json.SnakePickle
 import sttp.openai.requests.vectorstore.VectorStoreRequestBody.{CreateVectorStoreBody, ModifyVectorStoreBody}
-import sttp.openai.requests.vectorstore.VectorStoreResponseData.{
-  Completed,
-  DeleteVectorStoreResponse,
-  FileCounts,
-  InProgress,
-  ListVectorStoresResponse,
-  VectorStore
-}
+import sttp.openai.requests.vectorstore.VectorStoreResponseData._
+import sttp.openai.utils.JsonUtils
 
 class VectorStoreDataSpec extends AnyFlatSpec with Matchers with EitherValues {
 
@@ -85,7 +79,7 @@ class VectorStoreDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     val jsonResponse = VectorStoreFixture.jsonObject
 
     // when
-    val serializedJson: Either[Exception, VectorStore] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val serializedJson: Either[Exception, VectorStore] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     serializedJson.value shouldBe givenResponse
@@ -125,7 +119,7 @@ class VectorStoreDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     val jsonResponse = VectorStoreFixture.jsonList
 
     // when
-    val serializedJson: Either[Exception, ListVectorStoresResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val serializedJson: Either[Exception, ListVectorStoresResponse] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     serializedJson.value shouldBe givenResponse
@@ -144,7 +138,7 @@ class VectorStoreDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     val jsonResponse = VectorStoreFixture.jsonDelete
 
     // when
-    val serializedJson: Either[Exception, DeleteVectorStoreResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val serializedJson: Either[Exception, DeleteVectorStoreResponse] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     serializedJson.value shouldBe givenResponse
diff --git a/core/src/test/scala/sttp/openai/requests/vectorstore/file/VectorStoreFileDataSpec.scala b/core/src/test/scala/sttp/openai/requests/vectorstore/file/VectorStoreFileDataSpec.scala
index d311a73..fd9686d 100644
--- a/core/src/test/scala/sttp/openai/requests/vectorstore/file/VectorStoreFileDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/vectorstore/file/VectorStoreFileDataSpec.scala
@@ -4,16 +4,10 @@ import org.scalatest.EitherValues
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 import sttp.openai.fixtures.VectorStoreFileFixture
-import sttp.openai.json.{SnakePickle, SttpUpickleApiExtension}
+import sttp.openai.json.SnakePickle
 import sttp.openai.requests.vectorstore.file.VectorStoreFileRequestBody.{CreateVectorStoreFileBody, ListVectorStoreFilesBody}
-import sttp.openai.requests.vectorstore.file.VectorStoreFileResponseData.{
-  DeleteVectorStoreFileResponse,
-  LastError,
-  ListVectorStoreFilesResponse,
-  RateLimitExceeded,
-  ServerError,
-  VectorStoreFile
-}
+import sttp.openai.requests.vectorstore.file.VectorStoreFileResponseData._
+import sttp.openai.utils.JsonUtils
 
 class VectorStoreFileDataSpec extends AnyFlatSpec with Matchers with EitherValues {
 
@@ -65,7 +59,7 @@ class VectorStoreFileDataSpec extends AnyFlatSpec with Matchers with EitherValue
     val jsonResponse = VectorStoreFileFixture.jsonObject
 
     // when
-    val serializedJson: Either[Exception, VectorStoreFile] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val serializedJson: Either[Exception, VectorStoreFile] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     serializedJson.value shouldBe givenResponse
@@ -86,7 +80,7 @@ class VectorStoreFileDataSpec extends AnyFlatSpec with Matchers with EitherValue
     val jsonResponse = VectorStoreFileFixture.jsonObjectWithLastError
 
     // when
-    val serializedJson: Either[Exception, VectorStoreFile] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val serializedJson: Either[Exception, VectorStoreFile] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     serializedJson.value shouldBe givenResponse
@@ -125,7 +119,7 @@ class VectorStoreFileDataSpec extends AnyFlatSpec with Matchers with EitherValue
     val jsonResponse = VectorStoreFileFixture.jsonList
 
     // when
-    val serializedJson: Either[Exception, ListVectorStoreFilesResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val serializedJson: Either[Exception, ListVectorStoreFilesResponse] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     serializedJson.value shouldBe givenResponse
@@ -144,7 +138,7 @@ class VectorStoreFileDataSpec extends AnyFlatSpec with Matchers with EitherValue
     val jsonResponse = VectorStoreFileFixture.jsonDelete
 
     // when
-    val serializedJson: Either[Exception, DeleteVectorStoreFileResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(jsonResponse)
+    val serializedJson: Either[Exception, DeleteVectorStoreFileResponse] = JsonUtils.deserializeJsonSnake.apply(jsonResponse)
 
     // then
     serializedJson.value shouldBe givenResponse
diff --git a/core/src/test/scala/sttp/openai/utils/JsonUtils.scala b/core/src/test/scala/sttp/openai/utils/JsonUtils.scala
index 4a61a7f..86d5ca6 100644
--- a/core/src/test/scala/sttp/openai/utils/JsonUtils.scala
+++ b/core/src/test/scala/sttp/openai/utils/JsonUtils.scala
@@ -1,7 +1,23 @@
 package sttp.openai.utils
 
+import sttp.client4.{IsOption, JsonInput}
 import sttp.openai.json.SnakePickle.{read, write}
+import sttp.openai.json.SttpUpickleApiExtension.upickleApi
 
 object JsonUtils {
   def compactJson(json: String): String = write(read[ujson.Value](json))
+
+  def deserializeJsonSnake[B: upickleApi.Reader: IsOption]: String => Either[Exception, B] = { (s: String) =>
+    try
+      Right(upickleApi.read[B](JsonInput.sanitize[B].apply(s)))
+    catch {
+      case e: Exception => Left(e)
+      case t: Throwable =>
+        // in ScalaJS, ArrayIndexOutOfBoundsException exceptions are wrapped in org.scalajs.linker.runtime.UndefinedBehaviorError
+        t.getCause match {
+          case e: ArrayIndexOutOfBoundsException => Left(e)
+          case _                                 => throw t
+        }
+    }
+  }
 }
diff --git a/streaming/akka/src/test/scala/sttp/openai/streaming/akka/AkkaClientSpec.scala b/streaming/akka/src/test/scala/sttp/openai/streaming/akka/AkkaClientSpec.scala
index 14e7312..f4f7b5c 100644
--- a/streaming/akka/src/test/scala/sttp/openai/streaming/akka/AkkaClientSpec.scala
+++ b/streaming/akka/src/test/scala/sttp/openai/streaming/akka/AkkaClientSpec.scala
@@ -1,14 +1,14 @@
 package sttp.openai.streaming.akka
 
+import akka.NotUsed
 import akka.actor.ActorSystem
 import akka.stream.scaladsl._
 import akka.util.ByteString
-import akka.NotUsed
 import org.scalatest.EitherValues
 import org.scalatest.flatspec.AsyncFlatSpec
 import org.scalatest.matchers.should.Matchers
 import sttp.client4.akkahttp.AkkaHttpBackend
-import sttp.client4.testing.RawStream
+import sttp.client4.testing.ResponseStub
 import sttp.model.sse.ServerSentEvent
 import sttp.openai.OpenAI
 import sttp.openai.OpenAIExceptions.OpenAIException.DeserializationOpenAIException
@@ -25,7 +25,7 @@ class AkkaClientSpec extends AsyncFlatSpec with Matchers with EitherValues {
   for ((statusCode, expectedError) <- ErrorFixture.testData)
     s"Service response with status code: $statusCode" should s"return properly deserialized ${expectedError.getClass.getSimpleName}" in {
       // given
-      val akkaBackendStub = AkkaHttpBackend.stub.whenAnyRequest.thenRespondWithCode(statusCode, ErrorFixture.errorResponse)
+      val akkaBackendStub = AkkaHttpBackend.stub.whenAnyRequest.thenRespondAdjust(ErrorFixture.errorResponse, statusCode)
       val client = new OpenAI("test-token")
 
       val givenRequest = ChatBody(
@@ -43,7 +43,7 @@ class AkkaClientSpec extends AsyncFlatSpec with Matchers with EitherValues {
       caught.map { c =>
         c.getClass shouldBe expectedError.getClass
         c.message shouldBe expectedError.message
-        c.cause shouldBe expectedError.cause
+        c.cause.getClass shouldBe expectedError.cause.getClass
         c.code shouldBe expectedError.code
         c.param shouldBe expectedError.param
         c.`type` shouldBe expectedError.`type`
@@ -59,7 +59,7 @@ class AkkaClientSpec extends AsyncFlatSpec with Matchers with EitherValues {
       .single(invalidEvent.toString)
       .map(ByteString(_))
 
-    val akkaBackendStub = AkkaHttpBackend.stub.whenAnyRequest.thenRespond(RawStream(streamedResponse))
+    val akkaBackendStub = AkkaHttpBackend.stub.whenAnyRequest.thenRespond(ResponseStub.adjust(streamedResponse))
     val client = new OpenAI(authToken = "test-token")
 
     val givenRequest = ChatBody(
@@ -112,7 +112,7 @@ class AkkaClientSpec extends AsyncFlatSpec with Matchers with EitherValues {
   }
 
   private def assertStreamedCompletion(givenResponse: Source[ByteString, NotUsed], expectedResponse: Seq[ChatChunkResponse]) = {
-    val akkaBackendStub = AkkaHttpBackend.stub.whenAnyRequest.thenRespond(RawStream(givenResponse))
+    val akkaBackendStub = AkkaHttpBackend.stub.whenAnyRequest.thenRespond(ResponseStub.adjust(givenResponse))
     val client = new OpenAI(authToken = "test-token")
 
     val givenRequest = ChatBody(
diff --git a/streaming/fs2/src/test/scala/sttp/openai/streaming/fs2/Fs2ClientSpec.scala b/streaming/fs2/src/test/scala/sttp/openai/streaming/fs2/Fs2ClientSpec.scala
index ee22d3d..8492268 100644
--- a/streaming/fs2/src/test/scala/sttp/openai/streaming/fs2/Fs2ClientSpec.scala
+++ b/streaming/fs2/src/test/scala/sttp/openai/streaming/fs2/Fs2ClientSpec.scala
@@ -1,13 +1,13 @@
 package sttp.openai.streaming.fs2
 
+import cats.effect.IO
+import cats.effect.testing.scalatest.AsyncIOSpec
+import fs2.{text, Stream}
 import org.scalatest.EitherValues
 import org.scalatest.flatspec.AsyncFlatSpec
-import cats.effect.testing.scalatest.AsyncIOSpec
 import org.scalatest.matchers.should.Matchers
-import cats.effect.IO
-import fs2.{text, Stream}
 import sttp.client4.httpclient.fs2.HttpClientFs2Backend
-import sttp.client4.testing.RawStream
+import sttp.client4.testing.ResponseStub
 import sttp.model.sse.ServerSentEvent
 import sttp.openai.OpenAI
 import sttp.openai.OpenAIExceptions.OpenAIException.DeserializationOpenAIException
@@ -22,7 +22,7 @@ class Fs2ClientSpec extends AsyncFlatSpec with AsyncIOSpec with Matchers with Ei
   for ((statusCode, expectedError) <- ErrorFixture.testData)
     s"Service response with status code: $statusCode" should s"return properly deserialized ${expectedError.getClass.getSimpleName}" in {
       // given
-      val fs2BackendStub = HttpClientFs2Backend.stub[IO].whenAnyRequest.thenRespondWithCode(statusCode, ErrorFixture.errorResponse)
+      val fs2BackendStub = HttpClientFs2Backend.stub[IO].whenAnyRequest.thenRespondAdjust(ErrorFixture.errorResponse, statusCode)
       val client = new OpenAI("test-token")
 
       val givenRequest = ChatBody(
@@ -40,7 +40,7 @@ class Fs2ClientSpec extends AsyncFlatSpec with AsyncIOSpec with Matchers with Ei
       caught.asserting { c =>
         c.getClass shouldBe expectedError.getClass
         c.message shouldBe expectedError.message
-        c.cause shouldBe expectedError.cause
+        c.cause.getClass shouldBe expectedError.cause.getClass
         c.code shouldBe expectedError.code
         c.param shouldBe expectedError.param
         c.`type` shouldBe expectedError.`type`
@@ -57,7 +57,7 @@ class Fs2ClientSpec extends AsyncFlatSpec with AsyncIOSpec with Matchers with Ei
       .through(text.utf8.encode)
       .covary[IO]
 
-    val fs2BackendStub = HttpClientFs2Backend.stub[IO].whenAnyRequest.thenRespond(RawStream(streamedResponse))
+    val fs2BackendStub = HttpClientFs2Backend.stub[IO].whenAnyRequest.thenRespond(ResponseStub.adjust(streamedResponse))
     val client = new OpenAI(authToken = "test-token")
 
     val givenRequest = ChatBody(
@@ -114,7 +114,7 @@ class Fs2ClientSpec extends AsyncFlatSpec with AsyncIOSpec with Matchers with Ei
   }
 
   private def assertStreamedCompletion(givenResponse: Stream[IO, Byte], expectedResponse: Seq[ChatChunkResponse]) = {
-    val pekkoBackendStub = HttpClientFs2Backend.stub[IO].whenAnyRequest.thenRespond(RawStream(givenResponse))
+    val pekkoBackendStub = HttpClientFs2Backend.stub[IO].whenAnyRequest.thenRespond(ResponseStub.adjust(givenResponse))
     val client = new OpenAI(authToken = "test-token")
 
     val givenRequest = ChatBody(
diff --git a/streaming/ox/src/test/scala/sttp/openai/streaming/ox/OxClientSpec.scala b/streaming/ox/src/test/scala/sttp/openai/streaming/ox/OxClientSpec.scala
index 43dced0..74a4094 100644
--- a/streaming/ox/src/test/scala/sttp/openai/streaming/ox/OxClientSpec.scala
+++ b/streaming/ox/src/test/scala/sttp/openai/streaming/ox/OxClientSpec.scala
@@ -5,6 +5,7 @@ import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 import ox.{supervised, Ox}
 import sttp.client4.DefaultSyncBackend
+import sttp.client4.testing.ResponseStub
 import sttp.model.sse.ServerSentEvent
 import sttp.openai.OpenAI
 import sttp.openai.OpenAIExceptions.OpenAIException.DeserializationOpenAIException
@@ -21,7 +22,7 @@ class OxClientSpec extends AnyFlatSpec with Matchers with EitherValues {
   for ((statusCode, expectedError) <- ErrorFixture.testData)
     s"Service response with status code: $statusCode" should s"return properly deserialized ${expectedError.getClass.getSimpleName}" in {
       // given
-      val stub = DefaultSyncBackend.stub.whenAnyRequest.thenRespondWithCode(statusCode, ErrorFixture.errorResponse)
+      val stub = DefaultSyncBackend.stub.whenAnyRequest.thenRespondAdjust(ErrorFixture.errorResponse, statusCode)
       val client = new OpenAI("test-token")
 
       val givenRequest = ChatBody(
@@ -40,7 +41,7 @@ class OxClientSpec extends AnyFlatSpec with Matchers with EitherValues {
       // then
       caught.getClass shouldBe expectedError.getClass
       caught.message shouldBe expectedError.message
-      caught.cause shouldBe expectedError.cause
+      caught.cause.getClass shouldBe expectedError.cause.getClass
       caught.code shouldBe expectedError.code
       caught.param shouldBe expectedError.param
       caught.`type` shouldBe expectedError.`type`
@@ -53,7 +54,7 @@ class OxClientSpec extends AnyFlatSpec with Matchers with EitherValues {
 
     val streamedResponse = new ByteArrayInputStream(invalidEvent.toString.getBytes)
 
-    val stub = DefaultSyncBackend.stub.whenAnyRequest.thenRespond(streamedResponse)
+    val stub = DefaultSyncBackend.stub.whenAnyRequest.thenRespond(ResponseStub.adjust(streamedResponse))
     val client = new OpenAI(authToken = "test-token")
 
     val givenRequest = ChatBody(
@@ -119,7 +120,7 @@ class OxClientSpec extends AnyFlatSpec with Matchers with EitherValues {
   }
 
   private def assertStreamedCompletion(givenResponse: InputStream, expectedResponse: Seq[ChatChunkResponse])(using Ox) = {
-    val stub = DefaultSyncBackend.stub.whenAnyRequest.thenRespond(givenResponse)
+    val stub = DefaultSyncBackend.stub.whenAnyRequest.thenRespond(ResponseStub.adjust(givenResponse))
     val client = new OpenAI(authToken = "test-token")
 
     val givenRequest = ChatBody(
diff --git a/streaming/pekko/src/test/scala/sttp/openai/streaming/pekko/PekkoClientSpec.scala b/streaming/pekko/src/test/scala/sttp/openai/streaming/pekko/PekkoClientSpec.scala
index 18e2d1b..c644207 100644
--- a/streaming/pekko/src/test/scala/sttp/openai/streaming/pekko/PekkoClientSpec.scala
+++ b/streaming/pekko/src/test/scala/sttp/openai/streaming/pekko/PekkoClientSpec.scala
@@ -1,5 +1,6 @@
 package sttp.openai.streaming.pekko
 
+import org.apache.pekko.NotUsed
 import org.apache.pekko.actor.ActorSystem
 import org.apache.pekko.stream.scaladsl._
 import org.apache.pekko.util.ByteString
@@ -7,7 +8,7 @@ import org.scalatest.EitherValues
 import org.scalatest.flatspec.AsyncFlatSpec
 import org.scalatest.matchers.should.Matchers
 import sttp.client4.pekkohttp.PekkoHttpBackend
-import sttp.client4.testing.RawStream
+import sttp.client4.testing.ResponseStub
 import sttp.model.sse.ServerSentEvent
 import sttp.openai.OpenAI
 import sttp.openai.OpenAIExceptions.OpenAIException.DeserializationOpenAIException
@@ -17,7 +18,6 @@ import sttp.openai.requests.completions.chat.ChatChunkRequestResponseData.ChatCh
 import sttp.openai.requests.completions.chat.ChatChunkRequestResponseData.ChatChunkResponse.DoneEvent
 import sttp.openai.requests.completions.chat.ChatRequestBody.{ChatBody, ChatCompletionModel}
 import sttp.openai.utils.JsonUtils.compactJson
-import org.apache.pekko.NotUsed
 
 class PekkoClientSpec extends AsyncFlatSpec with Matchers with EitherValues {
   implicit val system: ActorSystem = ActorSystem()
@@ -25,7 +25,7 @@ class PekkoClientSpec extends AsyncFlatSpec with Matchers with EitherValues {
   for ((statusCode, expectedError) <- ErrorFixture.testData)
     s"Service response with status code: $statusCode" should s"return properly deserialized ${expectedError.getClass.getSimpleName}" in {
       // given
-      val pekkoBackendStub = PekkoHttpBackend.stub.whenAnyRequest.thenRespondWithCode(statusCode, ErrorFixture.errorResponse)
+      val pekkoBackendStub = PekkoHttpBackend.stub.whenAnyRequest.thenRespondAdjust(ErrorFixture.errorResponse, statusCode)
       val client = new OpenAI("test-token")
 
       val givenRequest = ChatBody(
@@ -43,7 +43,7 @@ class PekkoClientSpec extends AsyncFlatSpec with Matchers with EitherValues {
       caught.map { c =>
         c.getClass shouldBe expectedError.getClass
         c.message shouldBe expectedError.message
-        c.cause shouldBe expectedError.cause
+        c.cause.getClass shouldBe expectedError.cause.getClass
         c.code shouldBe expectedError.code
         c.param shouldBe expectedError.param
         c.`type` shouldBe expectedError.`type`
@@ -59,7 +59,7 @@ class PekkoClientSpec extends AsyncFlatSpec with Matchers with EitherValues {
       .single(invalidEvent.toString)
       .map(ByteString(_))
 
-    val pekkoBackendStub = PekkoHttpBackend.stub.whenAnyRequest.thenRespond(RawStream(streamedResponse))
+    val pekkoBackendStub = PekkoHttpBackend.stub.whenAnyRequest.thenRespond(ResponseStub.adjust(streamedResponse))
     val client = new OpenAI(authToken = "test-token")
 
     val givenRequest = ChatBody(
@@ -112,7 +112,7 @@ class PekkoClientSpec extends AsyncFlatSpec with Matchers with EitherValues {
   }
 
   private def assertStreamedCompletion(givenResponse: Source[ByteString, NotUsed], expectedResponse: Seq[ChatChunkResponse]) = {
-    val pekkoBackendStub = PekkoHttpBackend.stub.whenAnyRequest.thenRespond(RawStream(givenResponse))
+    val pekkoBackendStub = PekkoHttpBackend.stub.whenAnyRequest.thenRespond(ResponseStub.adjust(givenResponse))
     val client = new OpenAI(authToken = "test-token")
 
     val givenRequest = ChatBody(
diff --git a/streaming/zio/src/test/scala/sttp/openai/streaming/zio/ZioClientSpec.scala b/streaming/zio/src/test/scala/sttp/openai/streaming/zio/ZioClientSpec.scala
index 5c32745..26741d5 100644
--- a/streaming/zio/src/test/scala/sttp/openai/streaming/zio/ZioClientSpec.scala
+++ b/streaming/zio/src/test/scala/sttp/openai/streaming/zio/ZioClientSpec.scala
@@ -4,7 +4,7 @@ import org.scalatest.EitherValues
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 import sttp.client4.httpclient.zio.HttpClientZioBackend
-import sttp.client4.testing.RawStream
+import sttp.client4.testing.ResponseStub
 import sttp.model.sse.ServerSentEvent
 import sttp.openai.OpenAIExceptions.OpenAIException.DeserializationOpenAIException
 import sttp.openai.fixtures.ErrorFixture
@@ -23,7 +23,7 @@ class ZioClientSpec extends AnyFlatSpec with Matchers with EitherValues {
   for ((statusCode, expectedError) <- ErrorFixture.testData)
     s"Service response with status code: $statusCode" should s"return properly deserialized ${expectedError.getClass.getSimpleName}" in {
       // given
-      val zioBackendStub = HttpClientZioBackend.stub.whenAnyRequest.thenRespondWithCode(statusCode, ErrorFixture.errorResponse)
+      val zioBackendStub = HttpClientZioBackend.stub.whenAnyRequest.thenRespondAdjust(ErrorFixture.errorResponse, statusCode)
       val client = new OpenAI("test-token")
 
       val givenRequest = ChatBody(
@@ -42,7 +42,7 @@ class ZioClientSpec extends AnyFlatSpec with Matchers with EitherValues {
       // then
       caught.getClass shouldBe expectedError.getClass
       caught.message shouldBe expectedError.message
-      caught.cause shouldBe expectedError.cause
+      caught.cause.getClass shouldBe expectedError.cause.getClass
       caught.code shouldBe expectedError.code
       caught.param shouldBe expectedError.param
       caught.`type` shouldBe expectedError.`type`
@@ -57,7 +57,7 @@ class ZioClientSpec extends AnyFlatSpec with Matchers with EitherValues {
       .succeed(invalidEvent.toString)
       .via(ZPipeline.utf8Encode)
 
-    val zioBackendStub = HttpClientZioBackend.stub.whenAnyRequest.thenRespond(RawStream(streamedResponse))
+    val zioBackendStub = HttpClientZioBackend.stub.whenAnyRequest.thenRespond(ResponseStub.adjust(streamedResponse))
     val client = new OpenAI(authToken = "test-token")
 
     val givenRequest = ChatBody(
@@ -113,7 +113,7 @@ class ZioClientSpec extends AnyFlatSpec with Matchers with EitherValues {
   }
 
   private def assertStreamedCompletion(givenResponse: Stream[Throwable, Byte], expectedResponse: Seq[ChatChunkResponse]) = {
-    val zioBackendStub = HttpClientZioBackend.stub.whenAnyRequest.thenRespond(RawStream(givenResponse))
+    val zioBackendStub = HttpClientZioBackend.stub.whenAnyRequest.thenRespond(ResponseStub.adjust(givenResponse))
     val client = new OpenAI(authToken = "test-token")
 
     val givenRequest = ChatBody(