baikai 0.7.0.0 → 0.7.1.0
raw patch · 9 files changed
+415/−127 lines, 9 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Baikai.Models.Generated: anthropic_claude_opus_5_5 :: Model
+ Baikai.Models.Generated: openai_gpt_6_luna :: Model
+ Baikai.Models.Generated: openai_gpt_6_sol :: Model
Files
- CHANGELOG.md +98/−0
- baikai.cabal +121/−108
- fetch/FetchModelsCore.hs +34/−9
- src/Baikai/Models/Generated.hs +110/−0
- src/Baikai/Provider/Transport/Classify.hs +1/−0
- src/Baikai/Trace.hs +1/−0
- test/CatalogSpec.hs +14/−0
- test/FetchModelsSpec.hs +18/−8
- test/PricingPolicySpec.hs +18/−2
CHANGELOG.md view
@@ -7,6 +7,104 @@ ## [Unreleased] +## [baikai 0.7.1.0] - 2026-09-23++### Added++- Curated GPT-6 Sol and Luna on OpenAI Responses and Claude Opus 5.5 on+ Anthropic Messages (`openai_gpt_6_sol`, `openai_gpt_6_luna`,+ `anthropic_claude_opus_5_5`), with endpoint compatibility and standard,+ long-context, cache-duration, and fast-mode prices where applicable. All+ three passed live acceptance on 2026-09-23. Sol and Luna dispatch to+ `OpenAIResponses`, so calling them requires the+ `Baikai.Provider.OpenAI.Responses.register` call that `baikai-openai 0.7.0.0`+ introduced.++## [baikai-kit 0.3.0.0] - 2026-09-23++Closes four improvement requests from the tools that ship `baikai-kit` as their+`kit` command: project scope resolves from a configurable root (IR-8),+`kit status` reports local edits separately from upstream drift (IR-7),+`kit install` without a name asks a tool-supplied chooser (IR-6), and `list`,+`status` and `update` print versioned JSON (IR-9). A consumer raising its bound+builds `KitConfig` with `kitConfig`, passes it to `kitCommandParser`, and+matches on `StatusRow.conditions`; each break is at a call site the compiler+names.++### Added++- `baikai-kit`: `KitConfig.projectRoot :: IO FilePath` says where project scope+ lives. Install, status, update, uninstall and `agentDirsForSession` all derive+ project-scope paths from it, so they agree whichever subdirectory a command+ runs from. `kitConfig` builds a configuration with every optional field at its+ default (project scope is the current directory, as before);+ `projectRootByMarkers [".git", ".mytool"]` is a ready-made resolver that walks+ up to the nearest marker and falls back to the current directory, and+ `findProjectRoot` is the underlying walk. Resolves IR-8.++- `baikai-kit`: `kit status` reports local edits. It runs the same+ installed-file check `kit update` uses to skip an item, and shows+ `modified` for an edited copy and `edits-unknown` for one whose sidecar+ predates the installed-file hash. The check is exported as+ `checkLocalEdits`, returning `LocalEdits` (`Unedited`, `Edited`,+ `EditsUnknown`). Resolves IR-7.++- `baikai-kit`: `kit install` with no name asks a chooser the tool supplies in+ the new `KitConfig.chooseItem :: Maybe (KitManifest -> IO (Maybe Text))`+ field. The engine refreshes the kit, passes the whole manifest, and installs+ what the chooser returns; a cancelled choice prints+ `No item chosen; nothing installed.` and exits 0. With no chooser (the+ `kitConfig` default) the command fails with the new `KitItemNameRequired`+ error, which tells the user to pass `NAME`. The engine ships no picker.+ `KitCommand` derives `Eq`, and `kit install --help` names the tool's+ `.<tool>/agents` directory. Resolves IR-6.++- `baikai-kit`: `kit list`, `kit status` and `kit update` accept `--json` and+ print exactly one versioned JSON document on stdout+ (`{"formatVersion": 1, "document": "kit-list" | "kit-status" | "kit-update", …}`);+ warnings and the first-clone notice go to stderr, and a failed command writes+ nothing to stdout. The shapes are written by explicit encoders —+ `Baikai.Kit.Json.listDocument`, `statusDocument`, `updateDocument`, and+ `kitJsonFormatVersion` — so library callers get the same values, and they are+ pinned by golden tests. `Baikai.Kit.Command.OutputFormat` selects the mode,+ and `Baikai.Kit.Status.InstalledCopy` / `installedCopies` report where each+ item is installed. Resolves IR-9.++### Changed++- `baikai-kit`: `KitConfig` gains the strict field `projectRoot`, so a record+ literal must set it; build the configuration with+ `kitConfig toolName repoUrl providers` instead and override fields with record+ update syntax. `KitConfig`'s `Show` instance is now hand-written and prints+ `<IO FilePath>` for the resolver. __Breaking__.++- `baikai-kit`: `kit status` conditions compose, and `dirty` is renamed+ `changed-upstream` (it meant the upstream sources changed without a version+ bump, not local edits); `dirty+outdated` now reads+ `outdated+changed-upstream`. `StatusRow.state :: KitState` is replaced by+ `StatusRow.conditions :: [KitCondition]` (sorted; empty means up to date),+ `renderState` by `conditionLabel` and `renderConditions`, and `classify`+ returns `[KitCondition]`. `KitUpToDate`, `KitDirty` and `KitDirtyOutdated`+ are gone; match on the list instead. __Breaking__.++- `baikai-kit`: `KitInstall` takes `Maybe Text` (`Nothing` asks the chooser),+ `kitCommandParser` takes the `KitConfig` (migration: `kitCommandParser`+ becomes `kitCommandParser myKitConfig`), `KitConfig` gains the `chooseItem`+ field (set by `kitConfig`), and `KitError` gains `KitItemNameRequired`.+ __Breaking__.++- `baikai-kit`: `KitList`, `KitStatus` and `KitUpdate` gain a trailing+ `OutputFormat` field (`HumanOutput` for the previous behaviour).+ __Breaking__.++## [baikai-effectful 0.4.0.2] - 2026-09-15++### Changed (dependencies)++- Requires `effectful-core ^>=2.7` (was `^>=2.6`). No API change: none of the+ 2.7 breaking APIs (`LocalEnv`'s second type parameter, `SharedSuffix`,+ `KnownEffects`, the ticked strict modules) are used.+ ## [baikai 0.7.0.0] - 2026-09-08 ### Added
baikai.cabal view
@@ -1,7 +1,7 @@-cabal-version: 3.4-name: baikai-version: 0.7.0.0-synopsis: Unified Haskell interface for multiple AI providers+cabal-version: 3.4+name: baikai+version: 0.7.1.0+synopsis: Unified Haskell interface for multiple AI providers description: baikai provides a unified, provider-agnostic Haskell interface for working with multiple AI providers. It abstracts chat and streaming completions, tool@@ -9,21 +9,27 @@ concrete provider implementations supplied by companion packages such as @baikai-claude@ and @baikai-openai@. -category: AI-license: BSD-3-Clause-license-file: LICENSE-author: Nadeem Bitar-maintainer: nadeem@gmail.com-copyright: (c) 2026 Nadeem Bitar-build-type: Simple-tested-with: GHC ==9.12.4+category: AI+license: BSD-3-Clause+license-file: LICENSE+author: Nadeem Bitar+maintainer: nadeem@gmail.com+copyright: (c) 2026 Nadeem Bitar+build-type: Simple+tested-with: ghc ==9.12.4 extra-doc-files: CHANGELOG.md common common-options ghc-options:- -Wall -Wcompat -Widentities -Wincomplete-uni-patterns- -Wincomplete-record-updates -Wredundant-constraints- -fhide-source-paths -Wmissing-export-lists -Wpartial-fields+ -Wall+ -Wcompat+ -Widentities+ -Wincomplete-uni-patterns+ -Wincomplete-record-updates+ -Wredundant-constraints+ -fhide-source-paths+ -Wmissing-export-lists+ -Wpartial-fields -Wmissing-deriving-strategies -- Exhaustiveness is an error, not a warning. A non-exhaustive match@@ -38,10 +44,11 @@ -- fail the build on warnings that are stylistic or that a future GHC -- invents, and would push people toward blanket suppression. ghc-options:- -Werror=incomplete-patterns -Werror=incomplete-uni-patterns+ -Werror=incomplete-patterns+ -Werror=incomplete-uni-patterns -Werror=incomplete-record-updates - default-language: GHC2024+ default-language: GHC2024 default-extensions: DeriveAnyClass DuplicateRecordFields@@ -49,8 +56,8 @@ OverloadedStrings library- import: common-options- hs-source-dirs: src+ import: common-options+ hs-source-dirs: src exposed-modules: Baikai Baikai.Agent@@ -100,84 +107,90 @@ -- it so an evidence record can name the build that produced it, -- centrally rather than through a literal in each of the five -- packages that construct evidence.- other-modules: Paths_baikai+ other-modules: Paths_baikai autogen-modules: Paths_baikai build-depends:- , aeson ^>=2.2- , base >=4.20 && <5- , base16-bytestring ^>=1.0- , base64-bytestring ^>=1.2- , bytestring ^>=0.12- , case-insensitive ^>=1.2- , containers ^>=0.7- , cryptohash-sha256 ^>=0.11- , directory ^>=1.3- , filepath ^>=1.5- , generic-lens ^>=2.3- , http-client ^>=0.7- , http-client-tls >=0.3 && <0.5- , http-types ^>=0.12- , lens ^>=5.3- , openai ^>=2.5- , process ^>=1.6- , scientific ^>=0.3- , servant-client ^>=0.20- , stm ^>=2.5- , streamly >=0.11 && <0.13- , streamly-core >=0.3 && <0.5- , text ^>=2.1- , time ^>=1.14- , tls >=2.2 && <2.5- , unliftio-core ^>=0.2- , vector ^>=0.13+ aeson ^>=2.2,+ base >=4.20 && <5,+ base16-bytestring ^>=1.0,+ base64-bytestring ^>=1.2,+ bytestring ^>=0.12,+ case-insensitive ^>=1.2,+ containers ^>=0.7,+ cryptohash-sha256 ^>=0.11,+ directory ^>=1.3,+ filepath ^>=1.5,+ generic-lens ^>=2.3,+ http-client ^>=0.7,+ http-client-tls >=0.3 && <0.5,+ http-types ^>=0.12,+ lens ^>=5.3,+ openai ^>=2.5,+ process ^>=1.6,+ scientific ^>=0.3,+ servant-client ^>=0.20,+ stm ^>=2.5,+ streamly >=0.11 && <0.13,+ streamly-core >=0.3 && <0.5,+ text ^>=2.1,+ time ^>=1.14,+ tls >=2.2 && <2.5,+ unliftio-core ^>=0.2,+ vector ^>=0.13, executable baikai-gen-models- import: common-options+ import: common-options hs-source-dirs: gen- main-is: GenModels.hs- other-modules: GenModelsCore+ main-is: GenModels.hs+ other-modules: GenModelsCore build-depends:- , aeson ^>=2.2- , baikai- , base >=4.20 && <5- , bytestring ^>=0.12- , containers ^>=0.7- , directory ^>=1.3- , filepath ^>=1.5- , scientific ^>=0.3- , text ^>=2.1+ aeson ^>=2.2,+ baikai,+ base >=4.20 && <5,+ bytestring ^>=0.12,+ containers ^>=0.7,+ directory ^>=1.3,+ filepath ^>=1.5,+ scientific ^>=0.3,+ text ^>=2.1, executable baikai-fetch-models- import: common-options+ import: common-options hs-source-dirs: fetch- main-is: FetchModels.hs- other-modules: FetchModelsCore-+ main-is: FetchModels.hs+ other-modules: FetchModelsCore -- The main module is named @FetchModels@ (not @Main@) so the pure -- core can be compiled into the test suite alongside its own -- @Main.hs@ without a module-name clash.- ghc-options: -main-is FetchModels+ ghc-options:+ -main-is+ FetchModels+ build-depends:- , aeson ^>=2.2- , baikai- , base >=4.20 && <5- , bytestring ^>=0.12- , containers ^>=0.7- , directory ^>=1.3- , filepath ^>=1.5- , generic-lens ^>=2.3- , http-client ^>=0.7- , http-client-tls >=0.3 && <0.5- , lens ^>=5.3- , scientific ^>=0.3- , text ^>=2.1- , vector ^>=0.13+ aeson ^>=2.2,+ baikai,+ base >=4.20 && <5,+ bytestring ^>=0.12,+ containers ^>=0.7,+ directory ^>=1.3,+ filepath ^>=1.5,+ generic-lens ^>=2.3,+ http-client ^>=0.7,+ http-client-tls >=0.3 && <0.5,+ lens ^>=5.3,+ scientific ^>=0.3,+ text ^>=2.1,+ vector ^>=0.13, test-suite baikai-test- import: common-options- type: exitcode-stdio-1.0- hs-source-dirs: test fetch gen- main-is: Main.hs+ import: common-options+ type: exitcode-stdio-1.0+ hs-source-dirs:+ test+ fetch+ gen++ main-is: Main.hs other-modules: AgentAssetsSpec AgentSpec@@ -209,29 +222,29 @@ build-tool-depends: baikai:baikai-gen-models build-depends:- , aeson- , baikai- , base- , bytestring- , case-insensitive- , containers- , directory- , filepath- , generic-lens- , http-client- , http-types- , lens- , openai- , process- , scientific- , servant-client- , stm- , streamly-core >=0.3 && <0.5- , tasty- , tasty-hunit- , tasty-quickcheck- , temporary- , text- , time- , tls- , vector+ aeson,+ baikai,+ base,+ bytestring,+ case-insensitive,+ containers,+ directory,+ filepath,+ generic-lens,+ http-client,+ http-types,+ lens,+ openai,+ process,+ scientific,+ servant-client,+ stm,+ streamly-core >=0.3 && <0.5,+ tasty,+ tasty-hunit,+ tasty-quickcheck,+ temporary,+ text,+ time,+ tls,+ vector,
fetch/FetchModelsCore.hs view
@@ -259,13 +259,19 @@ } deriving stock (Generic) --- | Curation include set for OpenAI: the chat-completions-compatible--- current line. Responses-API-only ids (@*-pro@, @*-codex@,--- @*-deep-research@) are deliberately absent.+-- | Curation include set for OpenAI's current Chat and Responses lines.+-- Responses-only ids outside the text and function-tool scope+-- (@*-pro@, @*-codex@, @*-deep-research@) are deliberately absent. openaiInclude :: Map Text (Maybe CatalogModelCompat) openaiInclude =- Map.insert "gpt-6-astra" (Just (CatalogResponsesCompat astraResponsesFacts)) $- Map.fromList+ Map.union+ ( Map.fromList+ [ ("gpt-6-astra", Just (CatalogResponsesCompat astraResponsesFacts)),+ ("gpt-6-sol", Just (CatalogResponsesCompat gpt6ResponsesFacts)),+ ("gpt-6-luna", Just (CatalogResponsesCompat gpt6ResponsesFacts))+ ]+ )+ $ Map.fromList [ (model, Nothing) | model <- [ "gpt-5.6",@@ -293,6 +299,11 @@ ] ] where+ -- 2026-09-23: Sol and Luna require Responses for function calling at+ -- their default reasoning effort; Chat permits it only at effort none.+ -- https://developers.openai.com/api/docs/models/gpt-6-sol+ -- https://developers.openai.com/api/docs/models/gpt-6-luna+ gpt6ResponsesFacts = astraResponsesFacts -- 2026-09-07: native tools require Responses; only modern 30m cache TTL. -- https://developers.openai.com/api/docs/guides/latest-model astraResponsesFacts =@@ -322,6 +333,13 @@ -- docs/plans/60-... named this id as the one the include set did not -- yet carry, and stated the facts it would have to arrive with. ("claude-opus-5", fastAdaptive),+ -- 2026-09-23: always-on adaptive thinking; forced tool choice is+ -- rejected. Nondefault sampling is rejected on Claude 4.7 and later;+ -- fast mode is supported on the Claude API.+ -- https://platform.claude.com/docs/en/models/opus-5-5/whats-new-opus-5-5+ -- https://platform.claude.com/docs/en/claude_api_primer+ -- https://platform.claude.com/docs/en/build-with-claude/fast-mode+ ("claude-opus-5-5", opus55Facts), -- 2026-08-27: adaptive-only, sampling parameters rejected with a -- 400 — same source. ("claude-opus-4-8", fastAdaptive),@@ -353,8 +371,9 @@ ("claude-fable-5-1", adaptiveNoSampling & #supportsForcedToolChoice .~ False) ] where- -- 2026-09-07: all curated predecessors accept forced choice (subject to- -- their separate manual-thinking constraint); only Fable 5.1 rejects it.+ opus55Facts = adaptiveNoSampling & #supportsForcedToolChoice .~ False & #fastModeCost ?~ CatalogCost 8 40 0.4 10+ -- 2026-09-23: Fable 5.1 and Opus 5.5 reject forced choice; older curated+ -- models accept it subject to their separate manual-thinking constraint. -- https://platform.claude.com/docs/en/api/errors -- https://platform.claude.com/docs/en/models/fable-5-1/migration-guide -- 2026-09-07: Opus 5 and 4.8 only; cache multipliers stack on fast rates.@@ -620,17 +639,23 @@ where c = m ^. #cost --- | Provider documentation verified 2026-09-07. These rules supplement base+-- | Provider documentation verified 2026-09-23. These rules supplement base -- models.dev rates, which do not describe the full request billing policy. -- https://developers.openai.com/api/docs/models/gpt-6-astra+-- https://developers.openai.com/api/docs/models/gpt-6-sol+-- https://developers.openai.com/api/docs/models/gpt-6-luna -- https://platform.claude.com/docs/en/models/fable-5-1/overview+-- https://platform.claude.com/docs/en/models/opus-5-5/overview -- https://platform.claude.com/docs/en/build-with-claude/fast-mode pricingPolicies :: Map (Text, Text) Model.PricingPolicy pricingPolicies = Map.fromList- [ (("anthropic", "claude-opus-5"), Model.PricingPolicy [] (Just 10)),+ [ (("anthropic", "claude-opus-5-5"), Model.PricingPolicy [] (Just 8)),+ (("anthropic", "claude-opus-5"), Model.PricingPolicy [] (Just 10)), (("anthropic", "claude-opus-4-8"), Model.PricingPolicy [] (Just 10)), (("openai", "gpt-6-astra"), Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 20 75 2 25)] Nothing),+ (("openai", "gpt-6-sol"), Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 4 15 0.4 5)] Nothing),+ (("openai", "gpt-6-luna"), Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 0.2 0.75 0.02 0.25)] Nothing), (("anthropic", "claude-fable-5-1"), Model.PricingPolicy [] (Just 20)) ]
src/Baikai/Models/Generated.hs view
@@ -361,6 +361,49 @@ } } +anthropic_claude_opus_5_5 :: Model+anthropic_claude_opus_5_5 =+ emptyModel+ { modelId = "claude-opus-5-5",+ name = "Claude Opus 5.5",+ api = AnthropicMessages,+ provider = "anthropic",+ baseUrl = "https://api.anthropic.com",+ reasoning = True,+ input = [InputText, InputImage],+ cost =+ ModelCost+ { inputCost = 4 % 1,+ outputCost = 20 % 1,+ cacheReadCost = 1 % 5,+ cacheWriteCost = 5 % 1+ },+ fastModeCost =+ Just+ ( ModelCost+ { inputCost = 8 % 1,+ outputCost = 40 % 1,+ cacheReadCost = 2 % 5,+ cacheWriteCost = 10 % 1+ }+ ),+ pricingPolicy = Just (PricingPolicy [] (Just (8 % 1))),+ contextWindow = 1000000,+ maxOutputTokens = 128000,+ headers = Map.empty,+ compat =+ CompatAnthropicMessages+ defaultAnthropicMessagesCompat+ { supportsLongCacheRetention = True,+ supportsCacheControlOnTools = True,+ sendSessionAffinityHeaders = False,+ thinkingStyle = AnthropicThinkingAdaptive,+ supportsSamplingParameters = False,+ supportsFastMode = True,+ supportsForcedToolChoice = False+ }+ }+ anthropic_claude_sonnet_4_5 :: Model anthropic_claude_sonnet_4_5 = emptyModel@@ -998,6 +1041,70 @@ } } +openai_gpt_6_luna :: Model+openai_gpt_6_luna =+ emptyModel+ { modelId = "gpt-6-luna",+ name = "GPT-6 Luna",+ api = OpenAIResponses,+ provider = "openai",+ baseUrl = "https://api.openai.com",+ reasoning = True,+ input = [InputText, InputImage],+ cost =+ ModelCost+ { inputCost = 1 % 10,+ outputCost = 1 % 2,+ cacheReadCost = 1 % 100,+ cacheWriteCost = 1 % 8+ },+ fastModeCost = Nothing,+ pricingPolicy = Just (PricingPolicy [InputPriceTier 272000 (ModelCost (1 % 5) (3 % 4) (1 % 50) (1 % 4))] Nothing),+ contextWindow = 1050000,+ maxOutputTokens = 128000,+ headers = Map.empty,+ compat =+ CompatOpenAIResponses+ defaultOpenAIResponsesCompat+ { supportedReasoningEfforts = Just [ThinkingLow, ThinkingMedium, ThinkingHigh, ThinkingXHigh, ThinkingMax],+ supportsSamplingParameters = False,+ supportsLongCacheRetention = False,+ supportsPromptCacheOptions = True+ }+ }++openai_gpt_6_sol :: Model+openai_gpt_6_sol =+ emptyModel+ { modelId = "gpt-6-sol",+ name = "GPT-6 Sol",+ api = OpenAIResponses,+ provider = "openai",+ baseUrl = "https://api.openai.com",+ reasoning = True,+ input = [InputText, InputImage],+ cost =+ ModelCost+ { inputCost = 2 % 1,+ outputCost = 10 % 1,+ cacheReadCost = 1 % 5,+ cacheWriteCost = 5 % 2+ },+ fastModeCost = Nothing,+ pricingPolicy = Just (PricingPolicy [InputPriceTier 272000 (ModelCost (4 % 1) (15 % 1) (2 % 5) (5 % 1))] Nothing),+ contextWindow = 1050000,+ maxOutputTokens = 128000,+ headers = Map.empty,+ compat =+ CompatOpenAIResponses+ defaultOpenAIResponsesCompat+ { supportedReasoningEfforts = Just [ThinkingLow, ThinkingMedium, ThinkingHigh, ThinkingXHigh, ThinkingMax],+ supportsSamplingParameters = False,+ supportsLongCacheRetention = False,+ supportsPromptCacheOptions = True+ }+ }+ openai_o1 :: Model openai_o1 = emptyModel@@ -1159,6 +1266,7 @@ anthropic_claude_opus_4_7, anthropic_claude_opus_4_8, anthropic_claude_opus_5,+ anthropic_claude_opus_5_5, anthropic_claude_sonnet_4_5, anthropic_claude_sonnet_4_6, anthropic_claude_sonnet_5,@@ -1183,6 +1291,8 @@ openai_gpt_5_mini, openai_gpt_5_nano, openai_gpt_6_astra,+ openai_gpt_6_luna,+ openai_gpt_6_sol, openai_o1, openai_o3, openai_o3_mini,
src/Baikai/Provider/Transport/Classify.hs view
@@ -61,6 +61,7 @@ ePIPE, eTIMEDOUT, )+ -- Qualified: its 'IOErrorType' has a constructor named @OtherError@, -- which collides with the 'ErrorCategory' constructor of that name. import GHC.IO.Exception qualified as IOE
src/Baikai/Trace.hs view
@@ -65,6 +65,7 @@ summarizeContext, ) import Baikai.Error (BaikaiError, providerError)+ -- 'Baikai.Evidence.CallStatus' has a @CallFailed@ constructor and so -- does 'Baikai.Trace.Event.TraceEvent'. They mean different things and -- both belong in this module, so the status constructors stay behind
test/CatalogSpec.hs view
@@ -60,6 +60,19 @@ c.supportsSamplingParameters @?= False c.supportedReasoningEfforts @?= Just [ThinkingLow, ThinkingMedium, ThinkingHigh, ThinkingXHigh, ThinkingMax] _ -> assertFailure "Astra needs explicit OpenAI endpoint facts",+ testCase "Sol and Luna select Responses with explicit endpoint facts" $+ mapM_+ ( \mid -> do+ [api m | m <- allModels, modelId m == mid] @?= [OpenAIResponses]+ case [compat m | m <- allModels, modelId m == mid] of+ [CompatOpenAIResponses c] -> do+ c.supportsPromptCacheOptions @?= True+ c.supportsLongCacheRetention @?= False+ c.supportsSamplingParameters @?= False+ c.supportedReasoningEfforts @?= Just [ThinkingLow, ThinkingMedium, ThinkingHigh, ThinkingXHigh, ThinkingMax]+ _ -> assertFailure "GPT-6 Sol/Luna need explicit OpenAI Responses facts"+ )+ ["gpt-6-sol", "gpt-6-luna"], testCase "regenerating from data/models produces no diff" $ withSystemTempDirectory "baikai-catalog-spec" $ \tmpDir -> do let regenPath = tmpDir <> "/Generated.hs"@@ -103,6 +116,7 @@ ("claude-opus-4-7", (AnthropicThinkingAdaptive, False, True, False)), ("claude-opus-4-8", (AnthropicThinkingAdaptive, False, True, True)), ("claude-opus-5", (AnthropicThinkingAdaptive, False, True, True)),+ ("claude-opus-5-5", (AnthropicThinkingAdaptive, False, False, True)), ("claude-sonnet-4-5", (AnthropicThinkingBudget, True, True, False)), ("claude-sonnet-4-6", (AnthropicThinkingAdaptive, True, True, False)), ("claude-sonnet-5", (AnthropicThinkingAdaptive, False, True, False))
test/FetchModelsSpec.hs view
@@ -162,12 +162,15 @@ map (^. #apiOverride) (refreshed ^. #models) @?= [Just "openai-responses"] map (^. #compat) (refreshed ^. #models) @?= [expected] assertBool "explicit compat survives rendering" ("openai-responses" `Text.isInfixOf` decodeUtf8 (renderCatalog refreshed)),- testCase "fast rates and capability survive fetch and generator on exactly two curated models" $ do+ testCase "fast rates and capability survive fetch and generator on curated models" $ do upstream <- loadUpstream let sample = (upstream Map.! "openai") Map.! "gpt-5.4" forM_ (Map.keys anthropicInclude) $ \mid -> do let refreshed = normalizeProvider anthropicSpec (Map.singleton mid (sample & #modelId .~ mid))- expected = if mid `elem` ["claude-opus-5", "claude-opus-4-8"] then Just (CatalogCost 10 50 1 12.5) else Nothing+ expected+ | mid == "claude-opus-5-5" = Just (CatalogCost 8 40 0.4 10)+ | mid `elem` ["claude-opus-5", "claude-opus-4-8"] = Just (CatalogCost 10 50 1 12.5)+ | otherwise = Nothing map (^. #fastModeCost) (refreshed ^. #models) @?= [expected] case Aeson.eitherDecode (BSL.fromStrict (renderCatalog refreshed)) of Left err -> assertFailure err@@ -175,12 +178,19 @@ testCase "curated pricing survives fetch rendering and generator parsing" $ do upstream <- loadUpstream let sample = (upstream Map.! "openai") Map.! "gpt-5.4"- forM_ [(openaiSpec, "gpt-6-astra", Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 20 75 2 25)] Nothing), (anthropicSpec, "claude-fable-5-1", Model.PricingPolicy [] (Just 20))] $ \(spec, mid, policy) -> do- let refreshed = normalizeProvider spec (Map.singleton mid (sample & #modelId .~ mid))- map (^. #pricingPolicy) (refreshed ^. #models) @?= [Just policy]- case Aeson.eitherDecode (BSL.fromStrict (renderCatalog refreshed)) of- Left err -> assertFailure err- Right catalog -> map (Gen.pricingPolicy . snd) (Gen.flattenEntries catalog) @?= [Just policy],+ forM_+ [ (openaiSpec, "gpt-6-astra", Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 20 75 2 25)] Nothing),+ (openaiSpec, "gpt-6-sol", Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 4 15 0.4 5)] Nothing),+ (openaiSpec, "gpt-6-luna", Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 0.2 0.75 0.02 0.25)] Nothing),+ (anthropicSpec, "claude-fable-5-1", Model.PricingPolicy [] (Just 20)),+ (anthropicSpec, "claude-opus-5-5", Model.PricingPolicy [] (Just 8))+ ]+ $ \(spec, mid, policy) -> do+ let refreshed = normalizeProvider spec (Map.singleton mid (sample & #modelId .~ mid))+ map (^. #pricingPolicy) (refreshed ^. #models) @?= [Just policy]+ case Aeson.eitherDecode (BSL.fromStrict (renderCatalog refreshed)) of+ Left err -> assertFailure err+ Right catalog -> map (Gen.pricingPolicy . snd) (Gen.flattenEntries catalog) @?= [Just policy], testCase "OpenAI normalization filters, curates, and maps fields" $ do upstream <- loadUpstream catalogFor upstream openaiSpec @?= expectedOpenAI,
test/PricingPolicySpec.hs view
@@ -4,10 +4,11 @@ import Baikai.CacheRetention (CacheRetention (..)) import Baikai.Cost qualified as C-import Baikai.Cost.Pricing (computeCost, computeCostAtRates, computeCostForService, computeCostWith, resolveRates)+import Baikai.Cost.Pricing (computeCost, computeCostAtRates, computeCostAtSpeed, computeCostForService, computeCostWith, resolveRates) import Baikai.Evidence qualified as Ev import Baikai.Model qualified as M import Baikai.Models.Generated qualified as Models+import Baikai.Speed (Speed (..)) import Baikai.Usage qualified as U import Baikai.Usage.Normalize qualified as N import Control.Lens ((&), (.~))@@ -22,7 +23,22 @@ tests = testGroup "Pricing policy"- [ testCase "requested tiers never substitute for observed service" $ do+ [ testCase "Sol and Luna prices switch the complete request above 272K" $+ forM_ [(Models.openai_gpt_6_sol, M.ModelCost 2 10 (1 / 5) (5 / 2), M.ModelCost 4 15 (2 / 5) 5), (Models.openai_gpt_6_luna, M.ModelCost (1 / 10) (1 / 2) (1 / 100) (1 / 8), M.ModelCost (1 / 5) (3 / 4) (1 / 50) (1 / 4))] $ \(m, standard, highRates) -> do+ forM_ [(272000, standard), (272001, highRates)] $ \(n, expected) -> do+ let u = U.zeroUsage & #inputTokens .~ (n - 2000) & #cacheReadTokens .~ 1000 & #cacheWriteTokens .~ 1000 & #outputTokens .~ 100+ resolveRates Nothing m u @?= Right expected+ (computeCost m u).usd @?= (fromIntegral (n - 2000) * expected.inputCost + 1000 * expected.cacheReadCost + 1000 * expected.cacheWriteCost + 100 * expected.outputCost) / 1000000,+ testCase "Opus 5.5 prices short, long, and observed fast cache writes" $ do+ let m = Models.anthropic_claude_opus_5_5+ u = U.zeroUsage & #cacheWriteTokens .~ 1000000+ observedFast = U.observeBilling [U.BillingSpeed "fast", U.BillingServiceTier "standard"] u+ (computeCostWith (Just CacheRetentionShort) m u).usd @?= 5+ (computeCostWith (Just CacheRetentionLong) m u).usd @?= 8+ (computeCostAtSpeed m SpeedFast u).usd @?= 10+ (computeCostForService (Just CacheRetentionLong) Nothing m observedFast).usd @?= 16+ Set.member (C.UnsupportedSpeed "fast") (computeCostForService (Just CacheRetentionLong) Nothing m observedFast).basis.estimateReasons @?= False,+ testCase "requested tiers never substitute for observed service" $ do let unknown = N.normalizeUsage N.InclusiveInput (N.ReportedUsage (Just 1000) (Just 0) (Just 0) (Just 0) Nothing) standard = U.observeBilling [U.BillingServiceTier "default"] unknown priority = U.observeBilling [U.BillingServiceTier "priority"] unknown