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 +22/−9
- louter.cabal +1/−1
- src/Louter/Client.hs +42/−6
- src/Louter/Types/Response.hs +23/−1
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