packages feed

louter 0.1.0.0 → 0.1.1.0

raw patch · 4 files changed

+88/−17 lines, 4 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

+ Louter.Types.Response: FunctionCall :: !Text -> !Text -> FunctionCall
+ Louter.Types.Response: ResponseToolCall :: !Text -> !Text -> !FunctionCall -> ResponseToolCall
+ Louter.Types.Response: [choiceToolCalls] :: Choice -> ![ResponseToolCall]
+ Louter.Types.Response: [functionArguments] :: FunctionCall -> !Text
+ Louter.Types.Response: [functionName] :: FunctionCall -> !Text
+ Louter.Types.Response: [rtcFunction] :: ResponseToolCall -> !FunctionCall
+ Louter.Types.Response: [rtcId] :: ResponseToolCall -> !Text
+ Louter.Types.Response: [rtcType] :: ResponseToolCall -> !Text
+ Louter.Types.Response: data FunctionCall
+ Louter.Types.Response: data ResponseToolCall
+ Louter.Types.Response: instance Data.Aeson.Types.FromJSON.FromJSON Louter.Types.Response.FunctionCall
+ Louter.Types.Response: instance Data.Aeson.Types.FromJSON.FromJSON Louter.Types.Response.ResponseToolCall
+ Louter.Types.Response: instance Data.Aeson.Types.ToJSON.ToJSON Louter.Types.Response.FunctionCall
+ Louter.Types.Response: instance Data.Aeson.Types.ToJSON.ToJSON Louter.Types.Response.ResponseToolCall
+ Louter.Types.Response: instance GHC.Classes.Eq Louter.Types.Response.FunctionCall
+ Louter.Types.Response: instance GHC.Classes.Eq Louter.Types.Response.ResponseToolCall
+ Louter.Types.Response: instance GHC.Generics.Generic Louter.Types.Response.FunctionCall
+ Louter.Types.Response: instance GHC.Generics.Generic Louter.Types.Response.ResponseToolCall
+ Louter.Types.Response: instance GHC.Show.Show Louter.Types.Response.FunctionCall
+ Louter.Types.Response: instance GHC.Show.Show Louter.Types.Response.ResponseToolCall
- Louter.Types.Response: Choice :: !Int -> !Text -> !Maybe FinishReason -> Choice
+ Louter.Types.Response: Choice :: !Int -> !Text -> ![ResponseToolCall] -> !Maybe FinishReason -> Choice

Files

app/Main.hs view
@@ -706,15 +706,28 @@ -- | /v1/messages - Anthropic endpoint anthropicMessagesHandler :: AppState -> Application anthropicMessagesHandler state req respond = do+  -- Generate trace ID+  traceId <- generateTraceId+   -- Read request body   body <- strictRequestBody req    case eitherDecode body of-    Left err -> respond $ responseLBS status400-      [("Content-Type", "application/json")]-      (encode $ object ["error" .= ("Invalid Anthropic request: " <> T.pack err :: Text)])+    Left err -> do+      logEvent traceId "anthropic_parse_error" $ object+        [ "error" .= T.pack err+        , "body_preview" .= T.take 500 (TE.decodeUtf8 (BL.toStrict body))+        ]+      respond $ responseLBS status400+        [("Content-Type", "application/json")]+        (encode $ object ["error" .= ("Invalid Anthropic request: " <> T.pack err :: Text)])      Right anthropicReq -> do+      -- Log the full Anthropic request+      logEvent traceId "anthropic_request" $ object+        [ "request" .= anthropicReq+        ]+       -- Get first backend (for now - TODO: support backend selection via model name)       case Map.toList (configBackends $ appConfig state) of         [] -> respond $ responseLBS status500@@ -726,8 +739,8 @@           let isStreaming = getAnthropicStreamFlag anthropicReq            if isStreaming-            then handleAnthropicStreaming state backendCfg anthropicReq respond-            else handleAnthropicNonStreaming state backendCfg anthropicReq respond+            then handleAnthropicStreaming traceId state backendCfg anthropicReq respond+            else handleAnthropicNonStreaming traceId state backendCfg anthropicReq respond  -- | Get Anthropic stream flag getAnthropicStreamFlag :: Value -> Bool@@ -737,8 +750,8 @@ getAnthropicStreamFlag _ = False  -- | Handle Anthropic streaming request-handleAnthropicStreaming :: AppState -> BackendConfig -> Value -> (Response -> IO ResponseReceived) -> IO ResponseReceived-handleAnthropicStreaming state backendCfg anthropicReq respond = do+handleAnthropicStreaming :: Text -> AppState -> BackendConfig -> Value -> (Response -> IO ResponseReceived) -> IO ResponseReceived+handleAnthropicStreaming traceId state backendCfg anthropicReq respond = do   -- Convert Anthropic request to OpenAI format   case anthropicToOpenAI anthropicReq of     Left err -> respond $ responseLBS status400@@ -769,8 +782,8 @@       respond sseResponse  -- | Handle Anthropic non-streaming request-handleAnthropicNonStreaming :: AppState -> BackendConfig -> Value -> (Response -> IO ResponseReceived) -> IO ResponseReceived-handleAnthropicNonStreaming state backendCfg anthropicReq respond = do+handleAnthropicNonStreaming :: Text -> AppState -> BackendConfig -> Value -> (Response -> IO ResponseReceived) -> IO ResponseReceived+handleAnthropicNonStreaming traceId state backendCfg anthropicReq respond = do   case anthropicToOpenAI anthropicReq of     Left err -> respond $ responseLBS status400       [("Content-Type", "application/json")]
louter.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: louter-version: 0.1.0.0+version: 0.1.1.0 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,
src/Louter/Client.hs view
@@ -267,21 +267,57 @@     Just (Number n) -> Right (floor n)     _ -> Right 0 -  message <- case HM.lookup "message" choice of-    Just (Object msg) -> case HM.lookup "content" msg of-      Just (String txt) -> Right txt-      _ -> Right ""-    _ -> Right ""+  (message, toolCalls) <- case HM.lookup "message" choice of+    Just (Object msg) -> do+      let content = case HM.lookup "content" msg of+            Just (String txt) -> txt+            Just Null -> ""+            _ -> "" +      tools <- case HM.lookup "tool_calls" msg of+        Just (Array arr) -> mapM parseToolCall (V.toList arr)+        _ -> Right []++      Right (content, tools)+    _ -> Right ("", [])+   let finishReason = case HM.lookup "finish_reason" choice of         Just (String "stop") -> Just FinishStop         Just (String "length") -> Just FinishLength         Just (String "tool_calls") -> Just FinishToolCalls         _ -> Nothing -  pure $ Choice index message finishReason+  pure $ Choice index message toolCalls finishReason  parseOpenAIChoice _ = Left "Expected choice object"++-- | Parse a tool call from OpenAI format+parseToolCall :: Value -> Either String ResponseToolCall+parseToolCall (Object obj) = do+  tcId <- case HM.lookup "id" obj of+    Just (String i) -> Right i+    _ -> Left "Missing tool call id"++  tcType <- case HM.lookup "type" obj of+    Just (String t) -> Right t+    _ -> Right "function"++  tcFunction <- case HM.lookup "function" obj of+    Just (Object func) -> do+      name <- case HM.lookup "name" func of+        Just (String n) -> Right n+        _ -> Left "Missing function name"++      args <- case HM.lookup "arguments" func of+        Just (String a) -> Right a+        _ -> Right ""++      Right $ FunctionCall name args+    _ -> Left "Missing function object"++  pure $ ResponseToolCall tcId tcType tcFunction++parseToolCall _ = Left "Expected tool call object"  -- | Parse Anthropic format response (uses converter) parseAnthropicResponse :: BL.ByteString -> Either String ChatResponse
src/Louter/Types/Response.hs view
@@ -8,9 +8,11 @@   , Choice(..)   , FinishReason(..)   , Usage(..)+  , ResponseToolCall(..)+  , FunctionCall(..)   ) where -import Data.Aeson (FromJSON, ToJSON)+import Data.Aeson (FromJSON, ToJSON, Value) import Data.Text (Text) import GHC.Generics (Generic) @@ -29,11 +31,31 @@ data Choice = Choice   { choiceIndex :: !Int   , choiceMessage :: !Text          -- ^ Response text (or empty if tool call)+  , choiceToolCalls :: ![ResponseToolCall]  -- ^ Tool/function calls   , choiceFinishReason :: !(Maybe FinishReason)   } deriving (Show, Eq, Generic)  instance FromJSON Choice instance ToJSON Choice++-- | Tool/function call from the model (for non-streaming responses)+data ResponseToolCall = ResponseToolCall+  { rtcId :: !Text+  , rtcType :: !Text          -- ^ Usually "function"+  , rtcFunction :: !FunctionCall+  } deriving (Show, Eq, Generic)++instance FromJSON ResponseToolCall+instance ToJSON ResponseToolCall++-- | Function call details+data FunctionCall = FunctionCall+  { functionName :: !Text+  , functionArguments :: !Text     -- ^ JSON string of arguments+  } deriving (Show, Eq, Generic)++instance FromJSON FunctionCall+instance ToJSON FunctionCall  -- | Why the model stopped generating data FinishReason