baikai-openai 0.1.0.0 → 0.1.1.0
raw patch · 3 files changed
+75/−7 lines, 3 filesdep ~baikaiPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: baikai
API changes (from Hackage documentation)
+ Baikai.Provider.OpenAI.Api: mapRequest :: Model -> Context -> Options -> Either Text CreateChatCompletion
Files
- baikai-openai.cabal +5/−3
- src/Baikai/Provider/OpenAI/Api.hs +25/−1
- test/Main.hs +45/−3
baikai-openai.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.4 name: baikai-openai-version: 0.1.0.0+version: 0.1.1.0 synopsis: OpenAI providers for the baikai abstraction description: Wraps the openai Haskell package as a Baikai Provider for OpenAI's Chat Completions API.@@ -37,7 +37,7 @@ build-depends: , aeson- , baikai ^>=0.1.0+ , baikai ^>=0.1.1 , base >=4.20 && <5 , base64-bytestring , bytestring@@ -61,12 +61,14 @@ hs-source-dirs: test main-is: Main.hs build-depends:- , baikai ^>=0.1.0+ , aeson+ , baikai ^>=0.1.1 , baikai-openai , base >=4.20 && <5 , bytestring , generic-lens , lens ^>=5.3+ , openai , streamly-core >=0.3 && <0.5 , tasty , tasty-hunit
src/Baikai/Provider/OpenAI/Api.hs view
@@ -30,6 +30,7 @@ ( register, registerWithRegistry, openaiChatStream,+ mapRequest, ) where @@ -52,6 +53,7 @@ globalProviderRegistry, registerApiProviderWith, )+import Baikai.ResponseFormat (ResponseFormat (..)) import Baikai.StopReason qualified as Stop import Baikai.Stream (streamingComplete) import Baikai.Stream.Event@@ -95,6 +97,7 @@ import OpenAI.V1 qualified as OpenAI import OpenAI.V1.Chat.Completions qualified as Chat import OpenAI.V1.Models qualified as OpenAIModels+import OpenAI.V1.ResponseFormat qualified as RF import OpenAI.V1.Tool qualified as OpenAITool import OpenAI.V1.ToolCall qualified as ToolCall import Streamly.Data.Stream (Stream)@@ -726,6 +729,8 @@ toolChoiceField = fmap mkOpenAIToolChoice (opts ^. #toolChoice) reasoningEffortField = applyThinkingFormat compat (opts ^. #thinking)+ responseFormatField =+ fmap mkOpenAIResponseFormat (opts ^. #responseFormat) pure Chat._CreateChatCompletion { Chat.messages = Vector.fromList (prefix <> body),@@ -734,7 +739,26 @@ Chat.temperature = opts ^. #temperature, Chat.tools = toolsField, Chat.tool_choice = toolChoiceField,- Chat.reasoning_effort = reasoningEffortField+ Chat.reasoning_effort = reasoningEffortField,+ Chat.response_format = responseFormatField+ }++-- | Map a baikai 'ResponseFormat' onto the upstream OpenAI+-- 'RF.ResponseFormat'. 'JsonObject' becomes plain-JSON mode;+-- 'JsonSchema' becomes a named, optionally-strict schema. The+-- schema 'Value' is forwarded verbatim.+mkOpenAIResponseFormat :: ResponseFormat -> RF.ResponseFormat+mkOpenAIResponseFormat = \case+ JsonObject -> RF.JSON_Object+ JsonSchema {name = n, schema = s, strict = st} ->+ RF.JSON_Schema+ { RF.json_schema =+ RF.JSONSchema+ { RF.description = Nothing,+ RF.name = n,+ RF.schema = Just s,+ RF.strict = Just st+ } } -- | Map a 'Baikai.ThinkingLevel.ThinkingLevel' onto the OpenAI SDK's
test/Main.hs view
@@ -4,24 +4,66 @@ import Baikai.Provider.OpenAI.Api import Baikai.Provider.OpenAI.Interactive import Control.Lens ((&), (.~), (^.))+import Data.Aeson qualified as Aeson import Data.ByteString.Char8 qualified as BS8 import Data.Generics.Labels () import Data.Text qualified as Text import Data.Vector qualified as Vector+import OpenAI.V1.Chat.Completions qualified as Chat+import OpenAI.V1.ResponseFormat qualified as RF import Streamly.Data.Stream qualified as Stream import Test.Tasty (TestTree, defaultMain, testGroup)-import Test.Tasty.HUnit (assertBool, testCase, (@?=))+import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=)) main :: IO () main = defaultMain $ testGroup- "Baikai.Provider.OpenAI.Interactive"+ "Baikai.Provider.OpenAI" [ commandRenderingTest, promptRenderingTest, compatDetectionTest,- rejectsImageToolResultsTest+ rejectsImageToolResultsTest,+ responseFormatMappingTest ]++-- | A 'JsonSchema' on 'Options.responseFormat' maps onto the+-- upstream OpenAI @response_format@ as a named, strict JSON schema,+-- forwarding the schema 'Value' verbatim. Pure: 'mapRequest' is+-- 'Either Text Chat.CreateChatCompletion'.+responseFormatMappingTest :: TestTree+responseFormatMappingTest =+ testCase "responseFormat JsonSchema maps onto OpenAI response_format" $ do+ let model =+ _Model+ & #modelId .~ "gpt-4o-mini"+ & #api .~ OpenAIChatCompletions+ & #provider .~ "openai"+ personSchema =+ Aeson.object+ [ "type" Aeson..= ("object" :: Text.Text),+ "properties"+ Aeson..= Aeson.object+ [ "name" Aeson..= Aeson.object ["type" Aeson..= ("string" :: Text.Text)],+ "age" Aeson..= Aeson.object ["type" Aeson..= ("integer" :: Text.Text)]+ ],+ "required" Aeson..= (["name", "age"] :: [Text.Text]),+ "additionalProperties" Aeson..= False+ ]+ ctx = _Context+ opts =+ _Options+ & #responseFormat+ .~ Just (JsonSchema {name = "person", schema = personSchema, strict = True})+ case mapRequest model ctx opts of+ Left e -> assertFailure ("mapRequest failed: " <> Text.unpack e)+ Right req -> case Chat.response_format req of+ Just (RF.JSON_Schema {RF.json_schema = js}) -> do+ RF.name js @?= "person"+ RF.schema js @?= Just personSchema+ RF.strict js @?= Just True+ RF.description js @?= Nothing+ other -> assertFailure ("expected JSON_Schema, got: " <> show other) commandRenderingTest :: TestTree commandRenderingTest =