baikai-openai 0.7.1.1 → 0.7.2.0
raw patch · 12 files changed
+214/−36 lines, 12 filesdep ~baikaiPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: baikai
API changes (from Hackage documentation)
Files
- CHANGELOG.md +85/−0
- baikai-openai.cabal +4/−3
- src/Baikai/Provider/OpenAI/Cli.hs +3/−0
- src/Baikai/Provider/OpenAI/Internal/Request.hs +5/−0
- src/Baikai/Provider/OpenAI/Internal/Stream.hs +32/−29
- src/Baikai/Provider/OpenAI/Responses/Assembler.hs +1/−0
- src/Baikai/Provider/OpenAI/Responses/Request.hs +5/−0
- src/Baikai/Provider/OpenAI/Responses/Stream.hs +3/−0
- src/Baikai/Provider/OpenAI/Transport.hs +2/−1
- test/DataInputSpec.hs +66/−0
- test/LifecycleSpec.hs +5/−2
- test/Main.hs +3/−1
CHANGELOG.md view
@@ -7,6 +7,91 @@ ## [Unreleased] +## [baikai 0.9.0.0] - 2026-10-25++### Added++- `Baikai.Judgment` compiles closed-set schemas into ordered questions, refuses+ generation controls and validates neutral answers without normalizing provider+ probabilities or deriving confidence and expected scores.+- `UserData`, `AssistantData`/`DataContent`, user-data message helpers, atomic+ data events, and named full-response JSON codecs preserve structured answers.+- `TypeSafeSystemOne`, catalog-owned `OutputKind`/`isJudgmentOnly`, `InputData`,+ and `typesafe_jev_latest`/`typesafe_jev_preview`. Legacy model JSON defaults to+ generative output. Register the new `baikai-typesafe` provider explicitly.++### Changed (breaking)++- Public content/event sums and model, error, trace and log records gain fields+ or constructors. Update exhaustive matches and record construction on upgrade.+- Terminal traces and both call-log paths preserve structured answer payloads.+ Evidence schema 2.6 includes structured response commitments and typed request+ problems; existing digest rules are unchanged.++## [baikai-typesafe 0.1.0.0] - 2026-10-25++### Added++- Initial explicitly registered TypeSafe System One provider using the released+ `typesafe-hs ^>=0.1.0.0` wire client. Strict request and response validation,+ atomic synthetic streaming, truthful call evidence, bounded HTTP calls and an+ opt-in live test. No retry policy is embedded.+- Installation, schema, refusal and consumer migration guide, plus CAP-27.++## [baikai-claude 0.7.2.0] - 2026-10-25++### Changed++- Require `baikai ^>=0.9.0`, encode user data as canonical JSON text, and refuse+ structured assistant history before dispatch with a typed field path.++### Fixed++- The offline cancellation fixture waits until the response reader is active+ before cancelling, eliminating a fixed-startup-delay race under build load.++## [baikai-openai 0.7.2.0] - 2026-10-25++### Changed++- Require `baikai ^>=0.9.0`, encode user data as canonical JSON text, and refuse+ structured assistant history before dispatch with a typed field path across+ Chat Completions, Responses and batch CLI calls.++### Fixed++- The offline cancellation fixture waits until the response reader is active+ before cancelling, eliminating a fixed-startup-delay race under build load.++## [baikai-trace-otel 0.4.1.0] - 2026-10-25++### Added++- Export structured answer data as canonical JSON under+ `baikai.response.structured_data`, on success and failure terminals.++### Changed++- Require `baikai ^>=0.9.0` for the expanded trace protocol.++## [baikai-effectful 0.4.0.4] - 2026-10-25++### Changed++- Require `baikai ^>=0.9.0`. Dependency-only update; no package API change.++## [baikai-kit 0.4.0.2] - 2026-10-25++### Changed++- Require `baikai ^>=0.9.0`. Dependency-only update; no package API change.++## [baikai-agent 0.2.0.3] - 2026-10-25++### Changed++- Require `baikai ^>=0.9.0`. Dependency-only update; no package API change.+ ## [baikai 0.8.0.0] - 2026-10-10 ### Added
baikai-openai.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.4 name: baikai-openai-version: 0.7.1.1+version: 0.7.2.0 synopsis: OpenAI providers for the baikai abstraction description: OpenAI-compatible backends for baikai: Chat Completions over SSE against OpenAI and@@ -77,7 +77,7 @@ autogen-modules: Paths_baikai_openai build-depends: aeson ^>=2.2,- baikai ^>=0.8.0,+ baikai ^>=0.9.0, base >=4.20 && <5, base64-bytestring ^>=1.2, bytestring ^>=0.12,@@ -111,6 +111,7 @@ CliEvidenceSpec CliProcessFixture Contract+ DataInputSpec EndpointModels ErrorClassSpec EvidenceSpec@@ -130,7 +131,7 @@ build-depends: aeson,- baikai ^>=0.8.0,+ baikai ^>=0.9.0, baikai-openai, base >=4.20 && <5, bytestring,
src/Baikai/Provider/OpenAI/Cli.hs view
@@ -50,6 +50,7 @@ import Baikai.Api (Api (..)) import Baikai.Content (AssistantContent (..), TextContent (..)) import Baikai.Context (Context)+import Baikai.Context qualified as Context import Baikai.Error (BaikaiError, processError, providerError) import Baikai.Evidence qualified as Ev import Baikai.Evidence.Build qualified as Build@@ -70,6 +71,7 @@ import Baikai.ThinkingLevel (ThinkingLevel, renderThinkingLevel) import Baikai.Usage (Usage, zeroUsage) import Control.Exception (IOException, SomeException, bracket, displayException, fromException, try)+import Control.Exception qualified import Control.Lens ((&), (.~), (^.)) import Control.Monad (void) import Data.Aeson qualified as Aeson@@ -254,6 +256,7 @@ runCodexCli :: CodexCliConfig -> Model -> Context -> Options -> IO Resp.Response runCodexCli cfg m ctx opts = do+ either Control.Exception.throwIO pure (Context.refuseAssistantDataHistory ctx) let schema = requestedSchema opts -- The envelope names the schema itself where the spawned vector -- names its temporary file: the random path would make the
src/Baikai/Provider/OpenAI/Internal/Request.hs view
@@ -16,6 +16,8 @@ ) import Baikai.Content qualified as Content import Baikai.Context (Context (..))+import Baikai.Context qualified as Context+import Baikai.Error qualified as Error import Baikai.Message qualified as Msg import Baikai.Model (Model, openaiCompletionsCompatFor) import Baikai.Options (Options (..))@@ -49,6 +51,7 @@ mapRequest :: Model -> Context -> Options -> Either Text Chat.CreateChatCompletion mapRequest m ctx opts = do+ either (Left . Error.message) pure (Context.refuseAssistantDataHistory ctx) let capability = openaiCompletionsCompatFor m forced = case opts ^. #toolChoice of Just Tool.ToolChoiceRequired -> True@@ -272,6 +275,7 @@ userContentToPart :: Content.UserContent -> Chat.Content userContentToPart = \case Content.UserText (Content.TextContent t) -> Chat.Text {Chat.text = t}+ Content.UserData v -> Chat.Text {Chat.text = Content.canonicalDataText v} Content.UserImage img -> let encoded = Text.decodeUtf8 (Base64.encode (Content.imageData img)) uri = "data:" <> Content.mimeType img <> ";base64," <> encoded@@ -288,6 +292,7 @@ Content.AssistantText (Content.TextContent t) -> Just t Content.AssistantThinking _ -> Nothing Content.AssistantToolCall _ -> Nothing+ Content.AssistantData _ -> Nothing ) collectToolCalls :: Vector Content.AssistantContent -> Vector ToolCall.ToolCall
src/Baikai/Provider/OpenAI/Internal/Stream.hs view
@@ -45,6 +45,7 @@ import Baikai.Compat (OpenAICompletionsCompat (requiresThinkingAsText)) import Baikai.Content qualified as Content import Baikai.Context (Context (..))+import Baikai.Context qualified as Context import Baikai.Cost.Pricing qualified as Pricing import Baikai.Error (BaikaiError, contentFiltered, invalidRequest, providerError) import Baikai.Evidence qualified as Ev@@ -216,35 +217,37 @@ u -> u prepareCall :: Model -> Context -> Options -> IO (Either BaikaiError OpenAICall)-prepareCall m ctx opts = case mapRequest m ctx opts of- Left e -> pure (Left (invalidRequest e))- Right req -> do- let url = resolvedBaseUrl m- -- Checked before the key is resolved, so a base URL baikai will not- -- send to never causes a credential to be read out of the- -- environment. The message names the problem and what to write- -- instead; it renders the URL without its userinfo or query, so an- -- error reaching a log cannot carry a key someone put in either.- case Url.baseUrlProblem url of- Just problem ->- pure (Left (invalidRequest ("Model.baseUrl is not usable: " <> problem)))- Nothing -> do- key <- Transport.resolveKey url opts- env <- Transport.getClientEnvCached url- let compat = openaiCompletionsCompatFor m- (body, translation) = streamRequestBody compat (m ^. #reasoning) opts req- headers = Transport.requestHeaders key m opts- pure- ( Right- OpenAICall- { clientEnv = env,- requestHeaders = headers,- timeoutMs = opts ^. #timeoutMs,- requestBody = body,- baseUrl = url,- thinking = translation- }- )+prepareCall m ctx opts+ | Left e <- Context.refuseAssistantDataHistory ctx = pure (Left e)+ | otherwise = case mapRequest m ctx opts of+ Left e -> pure (Left (invalidRequest e))+ Right req -> do+ let url = resolvedBaseUrl m+ -- Checked before the key is resolved, so a base URL baikai will not+ -- send to never causes a credential to be read out of the+ -- environment. The message names the problem and what to write+ -- instead; it renders the URL without its userinfo or query, so an+ -- error reaching a log cannot carry a key someone put in either.+ case Url.baseUrlProblem url of+ Just problem ->+ pure (Left (invalidRequest ("Model.baseUrl is not usable: " <> problem)))+ Nothing -> do+ key <- Transport.resolveKey url opts+ env <- Transport.getClientEnvCached url+ let compat = openaiCompletionsCompatFor m+ (body, translation) = streamRequestBody compat (m ^. #reasoning) opts req+ headers = Transport.requestHeaders key m opts+ pure+ ( Right+ OpenAICall+ { clientEnv = env,+ requestHeaders = headers,+ timeoutMs = opts ^. #timeoutMs,+ requestBody = body,+ baseUrl = url,+ thinking = translation+ }+ ) -- | A loose summary of one streamed chunk. The raw 'Aeson.Value' is -- pre-parsed into the fields we care about; unknown fields are
src/Baikai/Provider/OpenAI/Responses/Assembler.hs view
@@ -287,6 +287,7 @@ deltaEvent _ n txt = E.TextDelta (E.DeltaPayload n txt) endEvent :: Int -> C.AssistantContent -> E.AssistantMessageEvent+endEvent n (C.AssistantData c) = E.DataEnd (E.DataEndPayload n c) endEvent n (C.AssistantThinking c) = E.ThinkingEnd (E.ThinkingEndPayload n c) endEvent n (C.AssistantToolCall c) = E.ToolCallEnd (E.ToolCallEndPayload n c) endEvent n (C.AssistantText c) = E.TextEnd (E.BlockEndPayload n c.text)
src/Baikai/Provider/OpenAI/Responses/Request.hs view
@@ -20,6 +20,8 @@ import Baikai.Compat (OpenAIResponsesCompat (..)) import Baikai.Content qualified as C import Baikai.Context (Context (..))+import Baikai.Context qualified as Context+import Baikai.Error qualified as Error import Baikai.Evidence qualified as E import Baikai.Message qualified as M import Baikai.Model (Model, api, maxOutputTokens, modelId, openaiResponsesCompatFor, reasoning)@@ -55,6 +57,7 @@ mapRequest :: Model -> Context -> Options -> Either Text PreparedRequest mapRequest m ctx opts = do+ either (Left . Error.message) pure (Context.refuseAssistantDataHistory ctx) unless (normaliseApi m.api == OpenAIResponses) (Left "Responses requires Model.api = OpenAIResponses") let compat = openaiResponsesCompatFor m unsupported = [name | (name, present) <- [("stopSequences", not (null opts.stopSequences)), ("seed", isJust opts.seed), ("frequencyPenalty", isJust opts.frequencyPenalty), ("presencePenalty", isJust opts.presencePenalty)], present]@@ -174,6 +177,7 @@ pure [Aeson.toJSON R.Item_Input_Function_Call_Output {R.id = Nothing, R.call_id = p.toolCallId, R.output = output, R.status = Nothing}] where assistantItem = \case+ C.AssistantData _ -> Left "Cannot replay AssistantData evidence" C.AssistantText t -> pure [object ["role" .= ("assistant" :: Text), "content" .= t.text]] C.AssistantToolCall tc -> do unless (not (Text.null tc.id_) && not (Text.null tc.name)) (Left "Responses tool calls need a nonempty call_id and name")@@ -188,6 +192,7 @@ userPart :: C.UserContent -> Value userPart = \case C.UserText t -> Aeson.toJSON (R.Input_Text t.text)+ C.UserData v -> Aeson.toJSON (R.Input_Text (C.canonicalDataText v)) C.UserImage img -> Aeson.toJSON R.Input_Image
src/Baikai/Provider/OpenAI/Responses/Stream.hs view
@@ -10,6 +10,7 @@ import Baikai.Content qualified as C import Baikai.Context (Context)+import Baikai.Context qualified as Context import Baikai.Cost.Pricing qualified as Pricing import Baikai.Error (BaikaiError, invalidRequest, providerError) import Baikai.Evidence qualified as Ev@@ -31,6 +32,7 @@ import Baikai.Usage qualified as U import Control.Applicative ((<|>)) import Control.Exception (SomeAsyncException, SomeException, fromException, throwIO, try)+import Control.Exception qualified import Control.Lens ((&), (.~), (^.)) import Data.Aeson (Value (..), object, (.=)) import Data.Aeson.Key (Key)@@ -56,6 +58,7 @@ openaiResponsesStreamWith :: SseDriver -> Model -> Context -> Options -> Stream IO E.AssistantMessageEvent openaiResponsesStreamWith driver m ctx opts = Stream.concatEffect $ do setup <- trySync $ do+ either Control.Exception.throwIO pure (Context.refuseAssistantDataHistory ctx) req <- either (throwIO . invalidRequest) pure (R.mapRequest m ctx opts) let url = resolvedUrl m case Url.baseUrlProblem url of
src/Baikai/Provider/OpenAI/Transport.hs view
@@ -83,7 +83,8 @@ httpStatus = Nothing, retryAfterSeconds = Nothing, exitCode = Nothing,- refusalCategory = Nothing+ refusalCategory = Nothing,+ requestProblem = Nothing } -- | Apply caller overrides over the provider's own headers.
+ test/DataInputSpec.hs view
@@ -0,0 +1,66 @@+module DataInputSpec (tests) where++import Baikai+import Baikai.Prelude+import Baikai.Provider.OpenAI.Cli qualified as Cli+import Baikai.Provider.OpenAI.Internal.Request qualified as R+import Baikai.Provider.OpenAI.Internal.Stream qualified as S+import Baikai.Provider.OpenAI.Responses.Request qualified as RR+import Baikai.Provider.OpenAI.Responses.Stream qualified as RS+import Control.Exception (try)+import Data.Aeson qualified as A+import Data.ByteString.Lazy.Char8 qualified as BL+import Data.IORef+import Data.Text qualified as T+import Data.Vector qualified as V+import Streamly.Data.Stream qualified as Stream+import Test.Tasty+import Test.Tasty.HUnit++modelFixture :: Model+modelFixture = emptyModel & #modelId .~ "fixture" & #api .~ OpenAIChatCompletions++dataFixture :: A.Value+dataFixture = A.object ["z" A..= [1 :: Int, 2], "a" A..= ("日本語" :: T.Text)]++history :: Context+history = contextOf [AssistantMessage ((emptyResponse ^. #message) & #content .~ V.singleton (AssistantData (DataContent A.Null Nothing Nothing Nothing)))]++tests :: TestTree+tests =+ testGroup+ "structured input and history"+ [ testCase "Responses sends structured input canonically and refuses assistant data before driver" $ do+ req <- either (assertFailure . T.unpack) pure (RR.mapRequest (modelFixture & #api .~ OpenAIResponses) (contextOf [userData dataFixture]) emptyOptions)+ let bytes = BL.unpack (A.encode (req ^. #requestBody))+ assertBool "canonical data reaches Responses wire" (T.pack (BL.unpack (A.encode (canonicalDataText dataFixture))) `T.isInfixOf` T.pack bytes)+ calls <- newIORef (0 :: Int)+ es <- Stream.toList (RS.openaiResponsesStreamWith (\_ _ _ _ _ -> modifyIORef' calls (+ 1)) (modelFixture & #api .~ OpenAIResponses) history emptyOptions)+ readIORef calls >>= (@?= 0)+ case [e | EventError p <- es, Just e <- [p ^. #errorInfo]] of+ [e] -> e ^. #requestProblem @?= Just (UnsupportedFeature "messages[0].content[0]")+ _ -> assertFailure (show es),+ testCase "actual wire request contains canonical JSON in block position" $ do+ let ctx = contextOf [UserMessage (UserPayload (V.fromList [UserText (TextContent "before"), UserData dataFixture, UserText (TextContent "after")]) Nothing)]+ req <- either (assertFailure . T.unpack) pure (R.mapRequest modelFixture ctx emptyOptions)+ let wire = A.toJSON req+ bytes = BL.unpack (A.encode wire)+ jsonText = BL.unpack (A.encode (canonicalDataText dataFixture))+ assertBool ("structured text missing: " <> bytes) (T.pack jsonText `T.isInfixOf` T.pack bytes),+ testCase "OpenAIChatCompletions refuses assistant data with zero driver calls" $ do+ calls <- newIORef (0 :: Int)+ let driver _ _ _ _ _ = modifyIORef' calls (+ 1)+ es <- Stream.toList (S.openaiChatStreamWith driver modelFixture history emptyOptions)+ readIORef calls >>= (@?= 0)+ case [e | EventError p <- es, Just e <- [p ^. #errorInfo]] of+ [e] -> e ^. #requestProblem @?= Just (UnsupportedFeature "messages[0].content[0]")+ _ -> assertFailure (show es),+ testCase "CLI request carries JSON and refuses data history before spawning" $ do+ let cfg = Cli.defaultCodexCliConfig & #executable .~ "/nonexistent-baikai-data-test"+ (_, args) = Cli.codexCliCommand cfg modelFixture (contextOf [userData dataFixture]) emptyOptions+ assertBool "canonical JSON reaches argument vector" (any (T.isInfixOf (canonicalDataText dataFixture) . T.pack) args)+ result <- try ((Cli.codexCliProvider cfg ^. #complete) modelFixture history emptyOptions) :: IO (Either BaikaiError Response)+ case result of+ Left e -> e ^. #requestProblem @?= Just (UnsupportedFeature "messages[0].content[0]")+ Right r -> assertFailure (show r)+ ]
test/LifecycleSpec.hs view
@@ -119,11 +119,14 @@ (streamWith backend (countingDriver backend reads' closedRef (Just gate)) (modelFor backend) emptyContext testOptions) ) putMVar outcome (r :: Either SomeException [AssistantMessageEvent])- threadDelay 100000+ -- Wait for the driver to enter its bracket and reach the blocked read.+ -- A startup delay can cancel before a response exists under build load.+ ready <- pollFor 200 10000 ((>= 4) <$> readIORef reads') throwTo tid ThreadKilled+ r <- takeMVar outcome+ assertBool "consumer reached the blocked response read before cancellation" ready released <- pollFor 100 10000 (readIORef closedRef) assertBool "cancellation releases the connection without a GC" released- r <- takeMVar outcome case r of Left e | Just ThreadKilled <- fromException e -> pure () other -> assertFailure ("expected the drained thread to die by ThreadKilled, got: " <> show (fmap length other))
test/Main.hs view
@@ -41,6 +41,7 @@ import Data.Generics.Labels () import Data.Text qualified as Text import Data.Vector qualified as Vector+import DataInputSpec qualified import ErrorClassSpec qualified import EvidenceSpec qualified import LifecycleSpec qualified@@ -71,7 +72,8 @@ defaultMain $ testGroup "Baikai.Provider.OpenAI"- [ commandRenderingTest,+ [ DataInputSpec.tests,+ commandRenderingTest, effortRenderingTests, safetyRefusalTest, refusesRejectedApprovalPoliciesTest,