packages feed

louter 0.1.1.1 → 0.1.1.2

raw patch · 3 files changed

+66/−7 lines, 3 filesPVP: minor bump suggested

API additions: PVP suggests at least a minor version bump

API changes (from Hackage documentation)

+ Louter.Client: instance GHC.Show.Show Louter.Client.Backend
+ Louter.Client: newClientWithTimeout :: Maybe Int -> Backend -> IO Client

Files

+ CHANGELOG.md view
@@ -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.
louter.cabal view
@@ -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:
src/Louter/Client.hs view
@@ -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