packages feed

baikai 0.1.0.0 → 0.1.1.0

raw patch · 8 files changed

+240/−5 lines, 8 filesdep +openaiPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: openai

API changes (from Hackage documentation)

+ Baikai.Embedding: EmbeddingModel :: !Text -> !Text -> !Maybe Natural -> !ApiKeySource -> EmbeddingModel
+ Baikai.Embedding: [apiKey] :: EmbeddingModel -> !ApiKeySource
+ Baikai.Embedding: [baseUrl] :: EmbeddingModel -> !Text
+ Baikai.Embedding: [dimensions] :: EmbeddingModel -> !Maybe Natural
+ Baikai.Embedding: [modelId] :: EmbeddingModel -> !Text
+ Baikai.Embedding: _EmbeddingModel :: EmbeddingModel
+ Baikai.Embedding: data EmbeddingModel
+ Baikai.Embedding: embed :: EmbeddingModel -> [Text] -> IO (Vector (Vector Double))
+ Baikai.Embedding: embedOne :: EmbeddingModel -> Text -> IO (Vector Double)
+ Baikai.Embedding: instance GHC.Internal.Show.Show Baikai.Embedding.EmbeddingModel
+ Baikai.Embedding: mkEmbeddingRequest :: EmbeddingModel -> Text -> CreateEmbeddings
+ Baikai.Embedding: openAIEmbeddingModel :: Text -> EmbeddingModel
+ Baikai.Options: [responseFormat] :: Options -> !Maybe ResponseFormat
+ Baikai.ResponseFormat: JsonObject :: ResponseFormat
+ Baikai.ResponseFormat: JsonSchema :: !Text -> !Value -> !Bool -> ResponseFormat
+ Baikai.ResponseFormat: [name] :: ResponseFormat -> !Text
+ Baikai.ResponseFormat: [schema] :: ResponseFormat -> !Value
+ Baikai.ResponseFormat: [strict] :: ResponseFormat -> !Bool
+ Baikai.ResponseFormat: data ResponseFormat
+ Baikai.ResponseFormat: instance Data.Aeson.Types.FromJSON.FromJSON Baikai.ResponseFormat.ResponseFormat
+ Baikai.ResponseFormat: instance Data.Aeson.Types.ToJSON.ToJSON Baikai.ResponseFormat.ResponseFormat
+ Baikai.ResponseFormat: instance GHC.Classes.Eq Baikai.ResponseFormat.ResponseFormat
+ Baikai.ResponseFormat: instance GHC.Internal.Generics.Generic Baikai.ResponseFormat.ResponseFormat
+ Baikai.ResponseFormat: instance GHC.Internal.Show.Show Baikai.ResponseFormat.ResponseFormat
- Baikai.Options: Options :: !Maybe Natural -> !Maybe Double -> !Maybe ApiKeySource -> !Maybe Int -> !Map Text Text -> !Map Text Value -> !Maybe ToolChoice -> !Maybe CacheRetention -> !Maybe ThinkingLevel -> Options
+ Baikai.Options: Options :: !Maybe Natural -> !Maybe Double -> !Maybe ApiKeySource -> !Maybe Int -> !Map Text Text -> !Map Text Value -> !Maybe ToolChoice -> !Maybe CacheRetention -> !Maybe ThinkingLevel -> !Maybe ResponseFormat -> Options

Files

CHANGELOG.md view
@@ -7,6 +7,39 @@  ## [Unreleased] +## [baikai 0.1.1.0] - 2026-06-12++### Added++- Added provider-agnostic `ResponseFormat` support on `Options`, including+  plain JSON-object mode and named JSON-schema mode.+- Added `Baikai.Embedding`, an OpenAI `/v1/embeddings` client for text+  embeddings.++## [baikai-claude 0.1.1.0] - 2026-06-12++### Added++- Mapped baikai `ResponseFormat` options onto Anthropic `output_config` for+  Claude API requests.+- Exported `mapRequest` for request-mapping tests and downstream inspection.++## [baikai-openai 0.1.1.0] - 2026-06-12++### Added++- Mapped baikai `ResponseFormat` options onto OpenAI Chat Completions+  `response_format`.+- Exported `mapRequest` for request-mapping tests and downstream inspection.++## [baikai-effectful 0.1.0.0] - 2026-06-12++### Added++- Initial release: effectful binding for baikai with the `Baikai` dynamic+  effect, `complete`, `streamCollect`, `streamEach`, and registry-backed+  interpreters.+ ## [baikai 0.1.0.0] - 2026-06-04  ### Added
baikai.cabal view
@@ -1,6 +1,6 @@ cabal-version:   3.4 name:            baikai-version:         0.1.0.0+version:         0.1.1.0 synopsis:        Unified Haskell interface for multiple AI providers description:   baikai provides a unified, provider-agnostic Haskell interface for working@@ -47,6 +47,7 @@     Baikai.Cost     Baikai.Cost.Log     Baikai.Cost.Pricing+    Baikai.Embedding     Baikai.Error     Baikai.Interactive     Baikai.Message@@ -58,6 +59,7 @@     Baikai.Provider.Cli.Internal     Baikai.Provider.Registry     Baikai.Response+    Baikai.ResponseFormat     Baikai.StopReason     Baikai.Stream     Baikai.Stream.Event@@ -76,6 +78,7 @@     , containers     , generic-lens     , lens               ^>=5.3+    , openai     , scientific     , streamly           >=0.11 && <0.13     , streamly-core      >=0.3  && <0.5@@ -108,6 +111,7 @@     AgentAssetsSpec     CatalogSpec     CostSpec+    EmbeddingSpec     InteractiveSpec     TraceSpec @@ -119,6 +123,7 @@     , bytestring     , directory     , filepath+    , openai     , process     , stm     , streamly-core  >=0.3 && <0.5
src/Baikai.hs view
@@ -23,6 +23,7 @@     -- * Per-API compat shims and call-time options     module Baikai.Compat,     module Baikai.CacheRetention,+    module Baikai.ResponseFormat,     module Baikai.ThinkingLevel,      -- * Provider registry@@ -49,6 +50,7 @@ import Baikai.Options import Baikai.Provider import Baikai.Response+import Baikai.ResponseFormat import Baikai.StopReason import Baikai.Stream import Baikai.Stream.Event
+ src/Baikai/Embedding.hs view
@@ -0,0 +1,105 @@+-- | A small, provider-neutral embeddings client over an OpenAI-compatible+-- @\/v1\/embeddings@ endpoint (EP-15).+--+-- baikai shipped no embeddings client; this is the first. It reuses the same+-- @openai@ SDK path the OpenAI /chat/ provider already uses+-- ('OpenAI.V1.getClientEnv' + 'OpenAI.V1.makeMethods') and the sibling+-- 'OpenAI.V1.createEmbeddings' method, plus baikai's own 'Baikai.Auth' for key+-- resolution. It is policy-free (a plain @IO@ client, no effect binding) — the+-- effect interpreter lives one layer up in shikumi, exactly as @baikai-effectful@+-- relates to the transport.+--+-- An embedding model is named by a bare provider model-id string (e.g.+-- @\"text-embedding-3-small\"@) plus a base URL inside 'EmbeddingModel'; there is+-- no @Api@ tag and no chat-catalog entry, because none of the chat 'Baikai.Model'+-- fields (context window, output tokens, chat pricing, modalities) are meaningful+-- for embeddings.+module Baikai.Embedding+  ( EmbeddingModel (..),+    _EmbeddingModel,+    openAIEmbeddingModel,+    mkEmbeddingRequest,+    embed,+    embedOne,+  )+where++import Baikai.Auth (ApiKeySource (..), resolveApiKey)+import Data.Text (Text)+import Data.Vector (Vector)+import Data.Vector qualified as V+import Numeric.Natural (Natural)+import OpenAI.V1 qualified as OpenAI+import OpenAI.V1.Embeddings qualified as Emb+import OpenAI.V1.Models qualified as OpenAIModels++-- | How to reach an embeddings endpoint and which model to ask for.+data EmbeddingModel = EmbeddingModel+  { -- | e.g. @\"text-embedding-3-small\"@+    modelId :: !Text,+    -- | e.g. @\"https:\/\/api.openai.com\"@ (empty = the OpenAI default)+    baseUrl :: !Text,+    -- | request a reduced dimensionality, or 'Nothing' for the model default+    dimensions :: !(Maybe Natural),+    -- | how to resolve the API key (from "Baikai.Auth")+    apiKey :: !ApiKeySource+  }+  deriving stock (Show)++-- | A blank embedding model; a record-update target for hand-built models. Keyed+-- on @OPENAI_API_KEY@ by default.+_EmbeddingModel :: EmbeddingModel+_EmbeddingModel =+  EmbeddingModel+    { modelId = "",+      baseUrl = "",+      dimensions = Nothing,+      apiKey = ApiKeyEnv "OPENAI_API_KEY"+    }++-- | The OpenAI default: @api.openai.com@, key from @OPENAI_API_KEY@, model-default+-- dimensionality.+openAIEmbeddingModel :: Text -> EmbeddingModel+openAIEmbeddingModel mid =+  _EmbeddingModel+    { modelId = mid,+      baseUrl = "https://api.openai.com",+      dimensions = Nothing,+      apiKey = ApiKeyEnv "OPENAI_API_KEY"+    }++-- | Build the OpenAI @\/v1\/embeddings@ request for a single input text. Pure and+-- exported so it can be unit-tested without a network. (@embed@ calls it.)+mkEmbeddingRequest :: EmbeddingModel -> Text -> Emb.CreateEmbeddings+mkEmbeddingRequest m t =+  Emb._CreateEmbeddings+    { Emb.input = t,+      Emb.model = OpenAIModels.Model (modelId m),+      Emb.dimensions = dimensions m+    }++-- | Embed a batch of texts: one vector per input text, in input order. The SDK's+-- @CreateEmbeddings.input@ is a single 'Text', so this loops one call per text. The+-- transport exception (a Servant client error) is let propagate — error remapping+-- is the consumer's job (in shikumi the interpreter wraps it as a typed error).+embed :: EmbeddingModel -> [Text] -> IO (Vector (Vector Double))+embed _ [] = pure V.empty+embed m texts = do+  key <- resolveApiKey (apiKey m)+  env <- OpenAI.getClientEnv (urlOf m)+  let create = OpenAI.createEmbeddings (OpenAI.makeMethods env key Nothing Nothing)+  V.fromList <$> traverse (embedText create) texts+  where+    embedText create t = do+      objs <- create (mkEmbeddingRequest m t)+      pure (Emb.embedding (V.head objs))++-- | Embed a single text.+embedOne :: EmbeddingModel -> Text -> IO (Vector Double)+embedOne m t = V.head <$> embed m [t]++-- | Substitute the OpenAI default for an empty base URL (as the chat provider does).+urlOf :: EmbeddingModel -> Text+urlOf m = case baseUrl m of+  "" -> "https://api.openai.com"+  u -> u
src/Baikai/Options.hs view
@@ -10,7 +10,9 @@ -- EP-4 added @toolChoice@. EP-5 adds @cacheRetention@ and @thinking@ -- (provider-agnostic preferences that each provider maps to its own -- primitive — see 'Baikai.CacheRetention' and 'Baikai.ThinkingLevel'--- for the mappings).+-- for the mappings). EP-2 (shikumi) adds @responseFormat@, the+-- provider-agnostic structured-output preference — see+-- 'Baikai.ResponseFormat'. module Baikai.Options   ( Options (..),     _Options,@@ -19,6 +21,7 @@  import Baikai.Auth (ApiKeySource) import Baikai.CacheRetention (CacheRetention)+import Baikai.ResponseFormat (ResponseFormat) import Baikai.ThinkingLevel (ThinkingLevel) import Baikai.Tool (ToolChoice) import Data.Aeson (ToJSON, Value)@@ -37,7 +40,8 @@     metadata :: !(Map Text Value),     toolChoice :: !(Maybe ToolChoice),     cacheRetention :: !(Maybe CacheRetention),-    thinking :: !(Maybe ThinkingLevel)+    thinking :: !(Maybe ThinkingLevel),+    responseFormat :: !(Maybe ResponseFormat)   }   deriving stock (Eq, Show, Generic)   deriving anyclass (ToJSON)@@ -53,5 +57,6 @@       metadata = Map.empty,       toolChoice = Nothing,       cacheRetention = Nothing,-      thinking = Nothing+      thinking = Nothing,+      responseFormat = Nothing     }
+ src/Baikai/ResponseFormat.hs view
@@ -0,0 +1,38 @@+{-# OPTIONS_GHC -Wno-partial-fields #-}++-- | Provider-agnostic structured-output preference.+--+-- Each provider maps the value to its own native mechanism:+-- OpenAI's @response_format@ and Anthropic's @output_config@.+-- 'Nothing' on 'Baikai.Options.responseFormat' means no+-- structured-output constraint (today's behaviour).+module Baikai.ResponseFormat+  ( ResponseFormat (..),+  )+where++import Data.Aeson (FromJSON, ToJSON, Value)+import Data.Text (Text)+import GHC.Generics (Generic)++-- | How to constrain the model's output.+data ResponseFormat+  = -- | Enforce a named JSON Schema. The 'schema' is a raw JSON+    --   Schema document (an aeson 'Value'), passed through verbatim;+    --   baikai never inspects or validates it. 'strict' requests the+    --   provider's strict schema-enforcement mode where available+    --   (OpenAI honours it; Anthropic structured outputs are always+    --   schema-enforcing and ignore it).+    JsonSchema+      { name :: !Text,+        schema :: !Value,+        strict :: !Bool+      }+  | -- | Plain-JSON mode: the model must emit syntactically valid JSON+    --   but is not constrained to a specific shape. Maps to OpenAI's+    --   @{"type":"json_object"}@; on Anthropic (whose structured+    --   outputs require a schema) it maps to a permissive+    --   @{"type":"object"}@ schema.+    JsonObject+  deriving stock (Eq, Show, Generic)+  deriving anyclass (FromJSON, ToJSON)
+ test/EmbeddingSpec.hs view
@@ -0,0 +1,34 @@+-- | Tests for the embeddings client (EP-15, M1).+--+-- The request-mapping test is hermetic: it asserts on the pure+-- 'mkEmbeddingRequest' (no network), proving the input text, model id, and+-- dimensions land where the OpenAI @\/v1\/embeddings@ wire expects them. The live+-- test is gated on @BAIKAI_EMBEDDING_LIVE=1@ (and a real @OPENAI_API_KEY@) so the+-- default run stays offline.+module EmbeddingSpec (tests) where++import Baikai.Embedding (embedOne, mkEmbeddingRequest, openAIEmbeddingModel)+import Data.Vector qualified as V+import OpenAI.V1.Embeddings qualified as Emb+import OpenAI.V1.Models qualified as OpenAIModels+import System.Environment (lookupEnv)+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit (testCase, (@?=))++tests :: TestTree+tests =+  testGroup+    "Baikai.Embedding"+    [ testCase "mkEmbeddingRequest maps input, model id, and dimensions" $ do+        let req = mkEmbeddingRequest (openAIEmbeddingModel "text-embedding-3-small") "hello"+        Emb.input req @?= "hello"+        OpenAIModels.text (Emb.model req) @?= "text-embedding-3-small"+        Emb.dimensions req @?= Nothing,+      testCase "live embedding returns a 1536-length vector" $ do+        live <- lookupEnv "BAIKAI_EMBEDDING_LIVE"+        case live of+          Just "1" -> do+            v <- embedOne (openAIEmbeddingModel "text-embedding-3-small") "hello"+            V.length v @?= 1536+          _ -> putStrLn "BAIKAI_EMBEDDING_LIVE not set; skipping live test"+    ]
test/Main.hs view
@@ -10,6 +10,7 @@ import Data.ByteString.Lazy.Char8 qualified as LBS8 import Data.Text qualified as Text import Data.Vector qualified as V+import EmbeddingSpec qualified import InteractiveSpec qualified import Streamly.Data.Stream qualified as Stream import Test.Tasty (TestTree, defaultMain, testGroup)@@ -76,6 +77,7 @@         AgentAssetsSpec.tests,         CatalogSpec.tests,         CostSpec.tests,+        EmbeddingSpec.tests,         InteractiveSpec.tests,         TraceSpec.tests       ]@@ -90,7 +92,18 @@       testCase "_Options defaults are zero-y" $ do         _Options ^. #maxTokens @?= Nothing         _Options ^. #temperature @?= Nothing-        _Options ^. #apiKey @?= Nothing,+        _Options ^. #apiKey @?= Nothing+        _Options ^. #responseFormat @?= Nothing,+      testCase "responseFormat round-trips through Options" $ do+        responseFormat (_Options & #responseFormat .~ Just JsonObject)+          @?= Just JsonObject+        let person =+              Aeson.object+                [ "type" Aeson..= ("object" :: Text)+                ]+            schemaFmt = JsonSchema {name = "person", schema = person, strict = True}+        responseFormat (_Options & #responseFormat .~ Just schemaFmt)+          @?= Just schemaFmt,       testCase "Options Show redacts literal API keys" $ do         let secret = "sk-baikai-secret-never-print"             opts = _Options & #apiKey .~ Just (ApiKeyLiteral secret)