packages feed

langchain-hs-openai 0.0.6.0 → 0.0.6.2

raw patch · 5 files changed

+68/−12 lines, 5 filesdep ~langchain-hs-corePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: langchain-hs-core

API changes (from Hackage documentation)

Files

CHANGELOG.md view
@@ -1,5 +1,12 @@ # Changelog for langchain-hs-openai +## 0.0.6.2 - 2026-11-14++> **Note**: `0.0.6.1` was skipped globally after `langchain-hs-ollama-v0.0.6.1`.++- Added incremental SSE tool-call delta accumulation and `LLMChunk "" (Just toolCall)` emission in `ChatModel.stream`.+- Coordinated monorepo release alignment to `0.0.6.2` and updated `langchain-hs-core` dependency bound to `>= 0.0.6.2 && < 0.0.7`.+ ## 0.0.6.0 - 2026-10-02  - Initial release. Split from the monolithic `langchain-hs` package.
README.md view
@@ -9,7 +9,7 @@  ```yaml # stack.yaml extra-deps-- langchain-hs-openai-0.0.6.0+- langchain-hs-openai-0.0.6.2 ```  ```yaml
langchain-hs-openai.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           langchain-hs-openai-version:        0.0.6.0+version:        0.0.6.2 synopsis:       OpenAI provider and embeddings for langchain-hs description:    OpenAI ChatModel provider, streaming support, structured output, and text embeddings integration for langchain-hs. Compatible with OpenAI and OpenAI-compatible endpoints (OpenRouter, Fireworks, Together AI). category:       AI@@ -49,7 +49,7 @@     , http-client-tls >=0.3 && <0.5     , http-conduit ==2.*     , http-types >=0.11 && <0.13-    , langchain-hs-core ==0.0.6.*+    , langchain-hs-core >=0.0.6.2 && <0.0.7     , mtl >=2.2 && <2.4     , openai >=2.2.1 && <3     , servant >=0.19 && <0.21
src/Langchain/Provider/OpenAI.hs view
@@ -201,7 +201,7 @@  instance Aeson.FromJSON OpenAIStreamToolCall where   parseJSON = Aeson.withObject "OpenAIStreamToolCall" $ \obj -> do-    streamToolCallIndex <- obj Aeson..: "index"+    streamToolCallIndex <- obj Aeson..:? "index" Aeson..!= 0     streamToolCallId <- obj Aeson..:? "id"     streamFunction <- obj Aeson..:? "function"     let streamToolCallName = streamFunction >>= parseMaybe (Aeson..: "name")@@ -327,7 +327,9 @@             , CC.refusal = Nothing             , CC.name = messageName msg             , CC.assistant_audio = Nothing-            , CC.tool_calls = V.fromList . map toOAIToolCall <$> messageToolCalls msg+            , CC.tool_calls = case messageToolCalls msg of+                Just tcs@(_ : _) -> Just $ V.fromList (map toOAIToolCall tcs)+                _ -> Nothing             }         Tool ->           CC.Tool@@ -453,13 +455,15 @@               Map.alter (Just . update) streamToolCallIndex toolCalls'               where                 update curr =-                  let prevId = curr >>= partialToolCallId+                  let nonEmptyTxt (Just t) | not (T.null t) = Just t+                      nonEmptyTxt _ = Nothing+                      prevId = curr >>= partialToolCallId                       prevName = curr >>= partialToolCallName                       prevArgs = maybe "" partialToolCallArguments curr                       nextArgs = fromMaybe "" streamToolCallArguments                    in PartialToolCall-                        { partialToolCallId = streamToolCallId <|> prevId-                        , partialToolCallName = streamToolCallName <|> prevName+                        { partialToolCallId = nonEmptyTxt streamToolCallId <|> prevId+                        , partialToolCallName = nonEmptyTxt streamToolCallName <|> prevName                         , partialToolCallArguments = prevArgs <> nextArgs                         } @@ -479,10 +483,13 @@                 toolCallName <-                   fromMaybeOrThrow "OpenAI stream ended with a tool call missing a function name" partialToolCallName                 toolCallArguments <--                  either-                    (throwLlmError . ("Invalid JSON arguments in OpenAI tool call: " <>) . T.pack)-                    pure-                    (decode partialToolCallArguments)+                  if T.null (T.strip partialToolCallArguments)+                    then pure (object [])+                    else+                      either+                        (throwLlmError . ("Invalid JSON arguments in OpenAI tool call: " <>) . T.pack)+                        pure+                        (decode partialToolCallArguments)                 pure ToolCall {toolCallId, toolCallType = "function", toolCallName, toolCallArguments}                throwLlmError = throwError . llmError'
test/Test/Langchain/Provider/OpenAI.hs view
@@ -406,6 +406,48 @@         case result of           Left _ -> pure ()           Right events -> assertFailure $ "Expected stream failure, got: " ++ show events+    , testCase "stream handles 0-argument tool call with empty arguments and empty delta id/name" $ do+        let frames =+              [ sseFrame+                  "{\"id\":\"chatcmpl-test\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"test-model\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_0\",\"type\":\"function\",\"function\":{\"name\":\"get_time\",\"arguments\":\"\"}}]},\"finish_reason\":null}]}"+              , sseFrame+                  "{\"id\":\"chatcmpl-test\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"test-model\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"\",\"function\":{\"name\":\"\",\"arguments\":\"\"}}]},\"finish_reason\":\"tool_calls\"}]}"+              , sseFrame "[DONE]"+              ]+            expectedToolCall = ToolCall "call_0" "function" "get_time" (Aeson.object [])+        result <- collectRawStream frames+        case result of+          Left err -> assertFailure $ "Expected stream success, got: " ++ show err+          Right events -> case events of+            [LLMStart {}, LLMChunk _ "" (Just tc), LLMEnd _ responseMessage Nothing] -> do+              tc @?= expectedToolCall+              messageToolCalls responseMessage @?= Just [expectedToolCall]+            _ -> assertFailure $ "Unexpected stream events: " ++ show events+    , testCase "stream accumulates parallel fragmented tool calls by index" $ do+        let frames =+              [ sseFrame+                  "{\"id\":\"chatcmpl-test\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"test-model\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_a\",\"type\":\"function\",\"function\":{\"name\":\"add\",\"arguments\":\"{\\\"x\\\":\"}}]},\"finish_reason\":null}]}"+              , sseFrame+                  "{\"id\":\"chatcmpl-test\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"test-model\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":1,\"id\":\"call_b\",\"type\":\"function\",\"function\":{\"name\":\"mul\",\"arguments\":\"{\\\"y\\\":\"}}]},\"finish_reason\":null}]}"+              , sseFrame+                  "{\"id\":\"chatcmpl-test\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"test-model\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"1}\"}},{\"index\":1,\"function\":{\"arguments\":\"2}\"}}]},\"finish_reason\":\"tool_calls\"}]}"+              , sseFrame "[DONE]"+              ]+            tcA = ToolCall "call_a" "function" "add" (Aeson.object ["x" Aeson..= (1 :: Int)])+            tcB = ToolCall "call_b" "function" "mul" (Aeson.object ["y" Aeson..= (2 :: Int)])+        result <- collectRawStream frames+        case result of+          Left err -> assertFailure $ "Expected stream success, got: " ++ show err+          Right events -> case events of+            [ LLMStart {}+              , LLMChunk _ "" (Just c0)+              , LLMChunk _ "" (Just c1)+              , LLMEnd _ responseMessage Nothing+              ] -> do+                c0 @?= tcA+                c1 @?= tcB+                messageToolCalls responseMessage @?= Just [tcA, tcB]+            _ -> assertFailure $ "Unexpected stream events: " ++ show events     , testCase "stream closes the SSE connection when the consumer stops after a chunk" $ do         clientClosed <- newEmptyMVar         withCancellationAwareProvider (putMVar clientClosed ()) $ \provider -> do