diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,20 @@
+# Changelog
+
+All notable changes to the `louter` Haskell library will be documented in this file.
+
+## 0.1.1.2 - 2026-04-30
+
+### Added
+- `newClientWithTimeout` entry point for creating a `Client` with a configurable HTTP response timeout (in seconds).
+- `Show` instances for `Backend` to aid debugging. API keys are redacted in the rendered output.
+
+### Changed
+- OpenAI request serialization now omits empty optional fields (`tools`, `temperature`, `max_tokens`) instead of emitting `null`, improving compatibility with backends that reject null-valued fields.
+- Single-text message content is now serialized as a plain string (`content: "..."`) instead of the typed-array form (`content: [{"type":"text","text":"..."}]`) when submitting requests.
+
+### Acknowledgments
+- Many thanks to [@julialongtin](https://github.com/julialongtin) (Julia Longtin) for contributing all of the changes in this release (PRs #1, #2, #3).
+
+## 0.1.1.1 - 2025-12-25
+
+Previous release.
diff --git a/louter.cabal b/louter.cabal
--- a/louter.cabal
+++ b/louter.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.0
 name: louter
-version: 0.1.1.1
+version: 0.1.1.2
 synopsis: Multi-protocol LLM router and client library
 description: Protocol converter library that lets your application connect to any LLM API
              (OpenAI, Gemini, Anthropic) with automatic protocol translation, SSE streaming,
@@ -14,6 +14,7 @@
 build-type: Simple
 
 extra-doc-files:    README.md
+                  , CHANGELOG.md
             
 library
   exposed-modules:
diff --git a/src/Louter/Client.hs b/src/Louter/Client.hs
--- a/src/Louter/Client.hs
+++ b/src/Louter/Client.hs
@@ -24,6 +24,7 @@
     Client
   , Backend(..)
   , newClient
+  , newClientWithTimeout
     -- * Simple API
   , chatCompletion
   , streamChat
@@ -38,7 +39,7 @@
 
 import Control.Monad (foldM)
 import Control.Monad.IO.Class (liftIO)
-import Data.Aeson (Value(..), encode, eitherDecode, object, (.=))
+import Data.Aeson (Value(..), encode, eitherDecode, object, toJSON, (.=))
 import qualified Data.Aeson.KeyMap as HM
 import Data.ByteString (ByteString)
 import qualified Data.ByteString as BS
@@ -108,12 +109,40 @@
       , backendRequiresAuth :: Bool
       }
 
+-- | Show instances, for debugging.
+instance Show Backend where
+  show (BackendOpenAI _ backendBaseURL backendAuthRequired) =
+    "BackendOpenAI" <>
+      "{ backendBaseURL = " <> show backendBaseURL <>
+      ", backendRequiresAuth = " <> show backendAuthRequired <>
+      ", backendApiKey = <redacted> }"
+  show (BackendGemini _ backendBaseURL backendAuthRequired) =
+    "BackendGemini" <>
+      "{ backendBaseURL = " <> show backendBaseURL <>
+      ", backendRequiresAuth = " <> show backendAuthRequired <>
+      ", backendApiKey = <redacted> }"
+  show (BackendOpenAI _ backendBaseURL backendAuthRequired) =
+    "BackendAnthropic" <>
+      "{ backendBaseURL = " <> show backendBaseURL <>
+      ", backendRequiresAuth = " <> show backendAuthRequired <>
+      ", backendApiKey = <redacted> }"
+
 -- | Create a new client
 newClient :: Backend -> IO Client
 newClient backend = do
   manager <- newManager tlsManagerSettings
   pure $ Client manager backend
 
+-- | Create a new client, with a set timeout.
+newClientWithTimeout :: Maybe Int -> Backend -> IO Client
+newClientWithTimeout timeout backend = do
+  let settings =
+        case timeout of
+          Nothing -> tlsManagerSettings
+          Just seconds -> tlsManagerSettings { managerResponseTimeout = responseTimeoutMicro (seconds * 1000000) }
+  manager <- newManager settings
+  pure $ Client manager backend
+
 -- | Non-streaming chat completion
 chatCompletion :: Client -> ChatRequest -> IO (Either Text ChatResponse)
 chatCompletion client req = do
@@ -340,19 +369,28 @@
             Nothing -> "https://api.openai.com/v1/chat/completions"
 
           -- Build OpenAI request format
+
+          -- Serialise as ```content: "quoted text here"```, rather than ```content: [{"type":"text", "text":"quoted text here"}]```
+          messageContent :: [ContentPart] -> Value
+          messageContent parts = case parts of
+                                   [TextPart text] -> String text
+                                   _               -> toJSON parts
+
           messagesJson = map (\msg -> object
             [ "role" .= msgRole msg
-            , "content" .= msgContent msg
+            , "content" .= messageContent (msgContent msg)
             ]) (reqMessages chatReq)
 
-          requestBody = encode $ object
+          -- Do not serialize empty members.
+          requestBody = encode $ object $
             [ "model" .= reqModel chatReq
             , "messages" .= messagesJson
-            , "tools" .= if null (reqTools chatReq) then Nothing else Just (reqTools chatReq)
-            , "temperature" .= reqTemperature chatReq
-            , "max_tokens" .= reqMaxTokens chatReq
             , "stream" .= reqStream chatReq
             ]
+            <> if null (reqTools chatReq) then [] else ["tools" .= reqTools chatReq]
+            <> if null (reqTemperature chatReq) then [] else ["temperature".= reqTemperature chatReq]
+            <> if null (reqMaxTokens chatReq) then [] else ["max_tokens" .= reqMaxTokens chatReq]
+
 
           headers = [(hContentType, "application/json")]
                  ++ if backendRequiresAuth
