packages feed

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 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