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 +20/−0
- louter.cabal +2/−1
- src/Louter/Client.hs +44/−6
+ 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