packages feed

baikai 0.9.0.0 → 0.10.0.0

raw patch · 19 files changed

+772/−134 lines, 19 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Baikai.Compat: NativeCacheLegacy :: !Bool -> !Bool -> NativeCachePolicy
+ Baikai.Compat: NativeCacheModern :: !Bool -> !Bool -> NativeCachePolicy
+ Baikai.Compat: NativeCacheUnsupported :: NativeCachePolicy
+ Baikai.Compat: data NativeCachePolicy
+ Baikai.Compat: instance Data.Aeson.Types.FromJSON.FromJSON Baikai.Compat.NativeCachePolicy
+ Baikai.Compat: instance Data.Aeson.Types.ToJSON.ToJSON Baikai.Compat.NativeCachePolicy
+ Baikai.Compat: instance GHC.Classes.Eq Baikai.Compat.NativeCachePolicy
+ Baikai.Compat: instance GHC.Internal.Generics.Generic Baikai.Compat.NativeCachePolicy
+ Baikai.Compat: instance GHC.Internal.Show.Show Baikai.Compat.NativeCachePolicy
+ Baikai.Compat: validateNativeCachePolicy :: Maybe CacheControlFormat -> Maybe NativeCachePolicy -> Either Text ()
+ Baikai.ThinkingLevel: ThinkingNone :: ThinkingLevel

Files

CHANGELOG.md view
@@ -7,6 +7,113 @@  ## [Unreleased] +## [baikai 0.10.0.0] - 2026-10-25++### Added++- `ThinkingNone` (`none`) explicitly requests reasoning off; `Nothing` still+  leaves provider defaults untouched. Anthropic compatibility records declare+  `supportsThinkingDisabled`; legacy records conservatively default to False.+- Endpoint-specific `NativeCachePolicy` records distinguish modern, legacy and+  unsupported native OpenAI cache controls. Policies survive legacy JSON and+  catalog refresh; Luna Responses Long has verified 12h base and tier rates.+- Evidence schema 2.8 includes the `none` preference and content-free native+  cache controls and input structure. Digest rules and the separation of+  requested, translated and observed reasoning are unchanged.++### Changed (breaking)++- `ThinkingLevel` gains `ThinkingNone` as its least constructor. Update exhaustive+  matches. Anthropic and OpenAI compatibility records gain fields; use the+  default records or update explicit record construction.+- All seven dependent packages now require `baikai ^>=0.10.0` and release+  independently below. The unchanged adapters take dependency-only patch bumps.++## [baikai-claude 0.7.3.0] - 2026-10-25++### Added++- Export `injectSystemCacheControl` for shared system-prefix cache markers.+- `ThinkingNone` sends catalog-gated `thinking: {"type": "disabled"}` without+  effort, display or a budget. Unsupported off requests remain visible+  adjustments and fail strict evidence before dispatch. A host with no thinking+  controls is not assumed to be a non-reasoning model.++### Fixed++- Short and long `cacheRetention` mark non-empty system prompts so fresh+  conversations with different first questions can reuse the tools-and-system+  prefix. System, last-tool and automatic markers share the effective TTL,+  including host downgrades, and use at most three breakpoints. Empty or absent+  system prompts and disabled caching keep their existing shape. Regression+  coverage includes tool compatibility and an isolated live shared-prefix case.+  Implements issue #2 and the approach in PR #3.++### Changed++- Require `baikai ^>=0.10.0`.++## [baikai-openai 0.7.3.0] - 2026-10-25++### Added++- Export `applyNativeCache` for validated native cache plans.+- OpenAI-compatible Chat (including Ollama) and Responses carry `none` where+  supported. Z.ai and Qwen emit `enable_thinking: false`; unsupported off+  requests remain visible adjustments and fail strict evidence before dispatch.++### Fixed++- Native OpenAI Short caching marks shared non-empty system prefixes on+  catalog-supported Responses and Chat bindings. Unsupported native requests+  fail locally. Effective duration reaches terminal pricing, partial usage+  and evidence.+- Funded live prefix checks pass for Luna Responses Short/Long and GPT-5.6 Chat+  Short. Luna Long uses verified 12h base and tier rates; unsupported Chat Long+  and unverified modern bindings remain locally refused.++### Changed++- Require `baikai ^>=0.10.0`. Catalog effort policies distinguish GPT-5/o-series+  models that cannot disable reasoning, Sol/Luna that accept none, and+  Astra/GPT-6.1 Sol that do not.++## [baikai-typesafe 0.1.0.1] - 2026-10-25++### Changed++- Require `baikai ^>=0.10.0`. Dependency-only update; no package API change.++## [baikai-trace-otel 0.4.1.1] - 2026-10-25++### Changed++- Require `baikai ^>=0.10.0`. Dependency-only update; no package API change.++## [baikai-effectful 0.4.0.5] - 2026-10-25++### Changed++- Require `baikai ^>=0.10.0`. Dependency-only update; no package API change.++## [baikai-kit 0.4.0.3] - 2026-10-25++### Changed++- Require `baikai ^>=0.10.0`. Dependency-only update; no package API change.++## [baikai-agent 0.2.0.4] - 2026-10-25++### Fixed++- Layered job configuration accepts `thinking "none"` through the shared+  canonical parser, keeping explicit off distinct from an absent preference.++### Changed++- Require `baikai ^>=0.10.0`, `baikai-claude ^>=0.7.3` and+  `baikai-openai ^>=0.7.3` for explicit-off handling in both vendor renderers.+ ## [baikai 0.9.0.0] - 2026-10-25  ### Added
baikai.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.4 name: baikai-version: 0.9.0.0+version: 0.10.0.0 synopsis: Unified Haskell interface for multiple AI providers description:   baikai provides a unified, provider-agnostic Haskell interface for working
data/models/anthropic.json view
@@ -22,7 +22,8 @@         "thinkingStyle": "adaptive",         "supportsSamplingParameters": false,         "supportsFastMode": false,-        "supportsForcedToolChoice": true+        "supportsForcedToolChoice": true,+        "supportsThinkingDisabled": false       },       "enabled": true     },@@ -45,7 +46,8 @@         "thinkingStyle": "adaptive",         "supportsSamplingParameters": false,         "supportsFastMode": false,-        "supportsForcedToolChoice": false+        "supportsForcedToolChoice": false,+        "supportsThinkingDisabled": false       },       "enabled": true     },@@ -67,7 +69,8 @@         "thinkingStyle": "budget",         "supportsSamplingParameters": true,         "supportsFastMode": false,-        "supportsForcedToolChoice": true+        "supportsForcedToolChoice": true,+        "supportsThinkingDisabled": true       },       "enabled": true     },@@ -90,7 +93,8 @@         "thinkingStyle": "adaptive",         "supportsSamplingParameters": false,         "supportsFastMode": false,-        "supportsForcedToolChoice": true+        "supportsForcedToolChoice": true,+        "supportsThinkingDisabled": true       },       "enabled": true     },@@ -112,7 +116,8 @@         "thinkingStyle": "budget",         "supportsSamplingParameters": true,         "supportsFastMode": false,-        "supportsForcedToolChoice": true+        "supportsForcedToolChoice": true,+        "supportsThinkingDisabled": true       },       "enabled": true     },@@ -134,7 +139,8 @@         "thinkingStyle": "adaptive",         "supportsSamplingParameters": true,         "supportsFastMode": false,-        "supportsForcedToolChoice": true+        "supportsForcedToolChoice": true,+        "supportsThinkingDisabled": true       },       "enabled": true     },@@ -156,7 +162,8 @@         "thinkingStyle": "adaptive",         "supportsSamplingParameters": false,         "supportsFastMode": false,-        "supportsForcedToolChoice": true+        "supportsForcedToolChoice": true,+        "supportsThinkingDisabled": true       },       "enabled": true     },@@ -180,7 +187,8 @@         "thinkingStyle": "adaptive",         "supportsSamplingParameters": false,         "supportsFastMode": true,-        "supportsForcedToolChoice": true+        "supportsForcedToolChoice": true,+        "supportsThinkingDisabled": true       },       "enabled": true     },@@ -204,7 +212,8 @@         "thinkingStyle": "adaptive",         "supportsSamplingParameters": false,         "supportsFastMode": true,-        "supportsForcedToolChoice": true+        "supportsForcedToolChoice": true,+        "supportsThinkingDisabled": true       },       "enabled": true     },@@ -228,7 +237,8 @@         "thinkingStyle": "adaptive",         "supportsSamplingParameters": false,         "supportsFastMode": true,-        "supportsForcedToolChoice": false+        "supportsForcedToolChoice": false,+        "supportsThinkingDisabled": false       },       "enabled": true     },@@ -250,7 +260,8 @@         "thinkingStyle": "budget",         "supportsSamplingParameters": true,         "supportsFastMode": false,-        "supportsForcedToolChoice": true+        "supportsForcedToolChoice": true,+        "supportsThinkingDisabled": true       },       "enabled": true     },@@ -272,7 +283,8 @@         "thinkingStyle": "adaptive",         "supportsSamplingParameters": true,         "supportsFastMode": false,-        "supportsForcedToolChoice": true+        "supportsForcedToolChoice": true,+        "supportsThinkingDisabled": true       },       "enabled": true     },@@ -294,7 +306,8 @@         "thinkingStyle": "adaptive",         "supportsSamplingParameters": false,         "supportsFastMode": false,-        "supportsForcedToolChoice": true+        "supportsForcedToolChoice": true,+        "supportsThinkingDisabled": true       },       "enabled": true     },@@ -317,7 +330,8 @@         "thinkingStyle": "adaptive",         "supportsSamplingParameters": false,         "supportsFastMode": false,-        "supportsForcedToolChoice": false+        "supportsForcedToolChoice": false,+        "supportsThinkingDisabled": false       },       "enabled": true     }
data/models/openai.json view
@@ -17,7 +17,11 @@       },       "contextWindow": 1047576,       "maxOutputTokens": 32768,-      "enabled": true+      "enabled": true,+      "compat": {+        "kind": "openai-completions",+        "nativeCachePolicy": {"tag": "NativeCacheLegacy", "contents": [true, true]}+      }     },     {       "id": "gpt-4.1-mini",@@ -92,6 +96,10 @@       },       "contextWindow": 400000,       "maxOutputTokens": 128000,+      "compat": {+        "kind": "openai-completions",+        "supportedReasoningEfforts": ["minimal", "low", "medium", "high"]+      },       "enabled": true     },     {@@ -107,6 +115,10 @@       },       "contextWindow": 400000,       "maxOutputTokens": 128000,+      "compat": {+        "kind": "openai-completions",+        "supportedReasoningEfforts": ["minimal", "low", "medium", "high"]+      },       "enabled": true     },     {@@ -122,6 +134,10 @@       },       "contextWindow": 400000,       "maxOutputTokens": 128000,+      "compat": {+        "kind": "openai-completions",+        "supportedReasoningEfforts": ["minimal", "low", "medium", "high"]+      },       "enabled": true     },     {@@ -212,7 +228,11 @@       },       "contextWindow": 1050000,       "maxOutputTokens": 128000,-      "enabled": true+      "enabled": true,+      "compat": {+        "kind": "openai-completions",+        "nativeCachePolicy": {"tag": "NativeCacheLegacy", "contents": [false, true]}+      }     },     {       "id": "gpt-5.6",@@ -227,7 +247,11 @@       },       "contextWindow": 1050000,       "maxOutputTokens": 128000,-      "enabled": true+      "enabled": true,+      "compat": {+        "kind": "openai-completions",+        "nativeCachePolicy": {"tag": "NativeCacheModern", "contents": [false, true]}+      }     },     {       "id": "gpt-5.6-luna",@@ -290,6 +314,7 @@       "api": "openai-responses",       "pricingPolicy": {"inputTiers": [{"inputAbove": 272000, "rates": {"input": 20.0, "output": 75.0, "cacheRead": 2.0, "cacheWrite": 25.0}}]},       "compat": {+        "nativeCachePolicy": {"tag": "NativeCacheModern", "contents": [false, true]},         "kind": "openai-responses",         "supportsSamplingParameters": false,         "supportsLongCacheRetention": false,@@ -312,13 +337,14 @@       "contextWindow": 1050000,       "maxOutputTokens": 128000,       "api": "openai-responses",-      "pricingPolicy": {"inputTiers": [{"inputAbove": 272000, "rates": {"input": 0.2, "output": 0.75, "cacheRead": 0.02, "cacheWrite": 0.25}}]},+      "pricingPolicy": {"inputTiers": [{"inputAbove": 272000, "rates": {"input": 0.2, "output": 0.75, "cacheRead": 0.02, "cacheWrite": 0.25}, "longCacheWriteCost": 0.5}], "longCacheWriteCost": 0.25},       "compat": {+        "nativeCachePolicy": {"tag": "NativeCacheModern", "contents": [true, true]},         "kind": "openai-responses",         "supportsSamplingParameters": false,-        "supportsLongCacheRetention": false,+        "supportsLongCacheRetention": true,         "supportsPromptCacheOptions": true,-        "supportedReasoningEfforts": ["low", "medium", "high", "xhigh", "max"]+        "supportedReasoningEfforts": ["none", "low", "medium", "high", "xhigh", "max"]       },       "enabled": true     },@@ -338,11 +364,12 @@       "api": "openai-responses",       "pricingPolicy": {"inputTiers": [{"inputAbove": 272000, "rates": {"input": 4.0, "output": 15.0, "cacheRead": 0.4, "cacheWrite": 5.0}}]},       "compat": {+        "nativeCachePolicy": {"tag": "NativeCacheModern", "contents": [false, true]},         "kind": "openai-responses",         "supportsSamplingParameters": false,         "supportsLongCacheRetention": false,         "supportsPromptCacheOptions": true,-        "supportedReasoningEfforts": ["low", "medium", "high", "xhigh", "max"]+        "supportedReasoningEfforts": ["none", "low", "medium", "high", "xhigh", "max"]       },       "enabled": true     },@@ -362,6 +389,7 @@       "api": "openai-responses",       "pricingPolicy": {"inputTiers": [{"inputAbove": 272000, "rates": {"input": 4.0, "output": 15.0, "cacheRead": 0.2, "cacheWrite": 5.0}}]},       "compat": {+        "nativeCachePolicy": {"tag": "NativeCacheModern", "contents": [false, true]},         "kind": "openai-responses",         "supportsSamplingParameters": false,         "supportsLongCacheRetention": false,@@ -383,6 +411,10 @@       },       "contextWindow": 200000,       "maxOutputTokens": 100000,+      "compat": {+        "kind": "openai-completions",+        "supportedReasoningEfforts": ["low", "medium", "high"]+      },       "enabled": true     },     {@@ -398,6 +430,10 @@       },       "contextWindow": 200000,       "maxOutputTokens": 100000,+      "compat": {+        "kind": "openai-completions",+        "supportedReasoningEfforts": ["low", "medium", "high"]+      },       "enabled": true     },     {@@ -413,6 +449,10 @@       },       "contextWindow": 200000,       "maxOutputTokens": 100000,+      "compat": {+        "kind": "openai-completions",+        "supportedReasoningEfforts": ["low", "medium", "high"]+      },       "enabled": true     },     {@@ -428,6 +468,10 @@       },       "contextWindow": 200000,       "maxOutputTokens": 100000,+      "compat": {+        "kind": "openai-completions",+        "supportedReasoningEfforts": ["low", "medium", "high"]+      },       "enabled": true     }   ]
fetch/FetchModelsCore.hs view
@@ -65,7 +65,7 @@   ) where -import Baikai.Compat (AnthropicThinkingStyle (..), OpenAICompletionsCompat (..), OpenAIResponsesCompat (..), defaultOpenAIResponsesCompat)+import Baikai.Compat (AnthropicThinkingStyle (..), NativeCachePolicy (..), OpenAICompletionsCompat (..), OpenAIResponsesCompat (..), defaultOpenAICompletionsCompat, defaultOpenAIResponsesCompat) import Baikai.Model (InputModality (..)) import Baikai.Model qualified as Model import Baikai.Prelude@@ -204,6 +204,8 @@   { thinkingStyle :: !AnthropicThinkingStyle,     supportsSamplingParameters :: !Bool,     supportsForcedToolChoice :: !Bool,+    -- | Whether the generation accepts @thinking: {"type": "disabled"}@.+    supportsThinkingDisabled :: !Bool,     fastModeCost :: !(Maybe CatalogCost)   }   deriving stock (Eq, Show, Generic)@@ -267,10 +269,20 @@ openaiInclude =   Map.union     ( Map.fromList-        [ ("gpt-6-astra", Just (CatalogResponsesCompat astraResponsesFacts)),+        [ ("gpt-5.6", Just (CatalogOpenAICompat (defaultOpenAICompletionsCompat & #nativeCachePolicy .~ Just (NativeCacheModern False True)))),+          ("gpt-5.5", Just (CatalogOpenAICompat (defaultOpenAICompletionsCompat & #nativeCachePolicy .~ Just (NativeCacheLegacy False True)))),+          ("gpt-4.1", Just (CatalogOpenAICompat (defaultOpenAICompletionsCompat & #nativeCachePolicy .~ Just (NativeCacheLegacy True True)))),+          ("gpt-5", Just (CatalogOpenAICompat gpt5Facts)),+          ("gpt-5-mini", Just (CatalogOpenAICompat gpt5Facts)),+          ("gpt-5-nano", Just (CatalogOpenAICompat gpt5Facts)),+          ("o1", Just (CatalogOpenAICompat oSeriesFacts)),+          ("o3", Just (CatalogOpenAICompat oSeriesFacts)),+          ("o3-mini", Just (CatalogOpenAICompat oSeriesFacts)),+          ("o4-mini", Just (CatalogOpenAICompat oSeriesFacts)),+          ("gpt-6-astra", Just (CatalogResponsesCompat astraResponsesFacts)),           ("gpt-6.1-sol", Just (CatalogResponsesCompat gpt61ResponsesFacts)),           ("gpt-6-sol", Just (CatalogResponsesCompat gpt6ResponsesFacts)),-          ("gpt-6-luna", Just (CatalogResponsesCompat gpt6ResponsesFacts))+          ("gpt-6-luna", Just (CatalogResponsesCompat lunaResponsesFacts))         ]     )     $ Map.fromList@@ -301,11 +313,25 @@           ]       ]   where+    -- 2026-10-25: Endpoint cache facts from the caching guide and Chat/Responses+    -- references: https://developers.openai.com/api/docs/guides/prompt-caching+    -- Short and breakpoint support passed live pairs on Luna Responses and+    -- GPT-5.6 Chat. A funded Luna Responses 12h probe completed and reported+    -- ttl_12h_tokens; GPT-5.6 Chat rejected 12h with HTTP 400 (30m only).+    -- Enable Long only on the probed Luna endpoint, with its published rates.+    -- https://developers.openai.com/api/docs/pricing+    lunaResponsesFacts = gpt6ResponsesFacts & #nativeCachePolicy .~ Just (NativeCacheModern True True) & #supportsLongCacheRetention .~ True     -- 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+    gpt6ResponsesFacts = astraResponsesFacts & #supportedReasoningEfforts .~ Just [ThinkingNone, ThinkingLow, ThinkingMedium, ThinkingHigh, ThinkingXHigh, ThinkingMax]+    -- 2026-10-25: GPT-5 accepts minimal through high, o-series low through+    -- high. Neither accepts none; keep this fact through catalog refreshes.+    -- https://developers.openai.com/api/docs/models/gpt-5+    -- https://developers.openai.com/api/docs/guides/reasoning+    gpt5Facts = defaultOpenAICompletionsCompat & #supportedReasoningEfforts .~ Just [ThinkingMinimal, ThinkingLow, ThinkingMedium, ThinkingHigh]+    oSeriesFacts = defaultOpenAICompletionsCompat & #supportedReasoningEfforts .~ Just [ThinkingLow, ThinkingMedium, ThinkingHigh]     -- 2026-09-29: as Astra, tool calling requires Responses; efforts low     -- through max (no none or minimal); sampling removed; 30m cache TTL.     -- https://developers.openai.com/api/docs/models/gpt-6.1-sol@@ -315,7 +341,8 @@     -- https://developers.openai.com/api/docs/guides/latest-model     astraResponsesFacts =       defaultOpenAIResponsesCompat-        { supportsPromptCacheOptions = True,+        { nativeCachePolicy = Just (NativeCacheModern False True),+          supportsPromptCacheOptions = True,           supportsLongCacheRetention = False,           supportsSamplingParameters = False,           supportedReasoningEfforts = Just [ThinkingLow, ThinkingMedium, ThinkingHigh, ThinkingXHigh, ThinkingMax]@@ -372,11 +399,11 @@       -- and sent it budget_tokens — same source.       ("claude-sonnet-5", adaptiveNoSampling),       -- 2026-09-29: adaptive thinking on by default; forced tool choice and-      -- nondefault sampling return 400; no fast mode. Baikai never sends-      -- the rejected "disabled" thinking type.+      -- nondefault sampling return 400; no fast mode. The "disabled" thinking+      -- type returns 400 too; its lowest setting is "between_tools".       -- https://platform.claude.com/docs/en/models/sonnet-5-5/whats-new-sonnet-5-5       -- https://platform.claude.com/docs/en/build-with-claude/fast-mode-      ("claude-sonnet-5-5", adaptiveNoSampling & #supportsForcedToolChoice .~ False),+      ("claude-sonnet-5-5", adaptiveNoSampling & #supportsForcedToolChoice .~ False & #supportsThinkingDisabled .~ False),       -- 2026-08-27: as claude-opus-4-6 — budget deprecated but functional,       -- sampling accepted; baikai prefers the non-deprecated shape — same       -- source. Plan 40 left this membership to a live check that never@@ -395,23 +422,27 @@       -- https://platform.claude.com/docs/en/build-with-claude/effort       ("claude-haiku-5-5", adaptiveNoSampling),       -- 2026-08-27: adaptive-only, sampling parameters rejected — same source.-      ("claude-fable-5", adaptiveNoSampling),+      ("claude-fable-5", adaptiveNoSampling & #supportsThinkingDisabled .~ False),       -- 2026-09-07: always-on adaptive thinking; omit sampling parameters.       -- https://platform.claude.com/docs/en/models/fable-5-1/whats-new-fable-5-1-      ("claude-fable-5-1", adaptiveNoSampling & #supportsForcedToolChoice .~ False)+      ("claude-fable-5-1", adaptiveNoSampling & #supportsForcedToolChoice .~ False & #supportsThinkingDisabled .~ False)     ]   where-    opus55Facts = adaptiveNoSampling & #supportsForcedToolChoice .~ False & #fastModeCost ?~ CatalogCost 8 40 0.4 10+    opus55Facts = adaptiveNoSampling & #supportsForcedToolChoice .~ False & #supportsThinkingDisabled .~ 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-10-19: Opus 5.5, Sonnet 5.5, Fable 5 and Fable 5.1 reject+    -- thinking {"type": "disabled"} with a 400; every other curated model+    -- accepts it (Haiku 5.5 and Opus 5 only at effort high or below).+    -- https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting     -- 2026-09-07: Opus 5 and 4.8 only; cache multipliers stack on fast rates.     -- https://platform.claude.com/docs/en/build-with-claude/fast-mode     fastAdaptive = adaptiveNoSampling & #fastModeCost ?~ CatalogCost 10 50 1 12.5-    adaptiveNoSampling = AnthropicGenerationFacts AnthropicThinkingAdaptive False True Nothing-    adaptiveWithSampling = AnthropicGenerationFacts AnthropicThinkingAdaptive True True Nothing-    budgetWithSampling = AnthropicGenerationFacts AnthropicThinkingBudget True True Nothing+    adaptiveNoSampling = AnthropicGenerationFacts AnthropicThinkingAdaptive False True True Nothing+    adaptiveWithSampling = AnthropicGenerationFacts AnthropicThinkingAdaptive True True True Nothing+    budgetWithSampling = AnthropicGenerationFacts AnthropicThinkingBudget True True True Nothing  -- | OpenAI curation with a Chat default and explicit Responses overrides. openaiSpec :: ProviderSpec@@ -695,7 +726,10 @@       (("anthropic", "claude-sonnet-5-5"), Model.PricingPolicy [] (Just 4)),       (("openai", "gpt-6.1-sol"), Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 4 15 0.2 5) Nothing] Nothing),       (("openai", "gpt-6-sol"), Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 4 15 0.4 5) Nothing] Nothing),-      (("openai", "gpt-6-luna"), Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 0.2 0.75 0.02 0.25) Nothing] Nothing),+      -- 2026-10-25: Luna Responses accepts 12h. The standard pricing table+      -- publishes 0.25/M below/at 272K and 0.50/M above, matching the+      -- guide's total 2.5x uncached-input rate (not an added surcharge).+      (("openai", "gpt-6-luna"), Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 0.2 0.75 0.02 0.25) (Just 0.5)] (Just 0.25)),       (("anthropic", "claude-fable-5-1"), Model.PricingPolicy [] (Just 20)),       (("anthropic", "claude-haiku-5-5"), Model.PricingPolicy [Model.InputPriceTier 100000 (Model.ModelCost 0.5 2.5 0.05 0.625) (Just 1)] (Just 0.2))     ]@@ -725,7 +759,8 @@       <> jsonBool (facts ^. #supportsSamplingParameters)       <> ",",     "        \"supportsFastMode\": " <> jsonBool (maybe False (const True) (facts ^. #fastModeCost)) <> ",",-    "        \"supportsForcedToolChoice\": " <> jsonBool (facts ^. #supportsForcedToolChoice),+    "        \"supportsForcedToolChoice\": " <> jsonBool (facts ^. #supportsForcedToolChoice) <> ",",+    "        \"supportsThinkingDisabled\": " <> jsonBool (facts ^. #supportsThinkingDisabled),     "      },"   ] renderModelCompat (Just (CatalogOpenAICompat facts)) =@@ -733,6 +768,7 @@     "        \"kind\": \"openai-completions\",",     "        \"supportsToolCalls\": " <> jsonBool (facts ^. #supportsToolCalls) <> ",",     "        \"supportsSamplingParameters\": " <> jsonBool (facts ^. #supportsSamplingParameters) <> ",",+    "        \"nativeCachePolicy\": " <> maybe "null" (decodeUtf8 . LBS.toStrict . encode) (facts ^. #nativeCachePolicy) <> ",",     "        \"supportedReasoningEfforts\": " <> maybe "null" (\xs -> "[" <> Text.intercalate ", " (map (jsonString . renderThinkingLevel) xs) <> "]") (facts ^. #supportedReasoningEfforts),     "      },"   ]@@ -742,6 +778,7 @@     "        \"supportsSamplingParameters\": " <> jsonBool (facts ^. #supportsSamplingParameters) <> ",",     "        \"supportsLongCacheRetention\": " <> jsonBool (facts ^. #supportsLongCacheRetention) <> ",",     "        \"supportsPromptCacheOptions\": " <> jsonBool (facts ^. #supportsPromptCacheOptions) <> ",",+    "        \"nativeCachePolicy\": " <> maybe "null" (decodeUtf8 . LBS.toStrict . encode) (facts ^. #nativeCachePolicy) <> ",",     "        \"supportedReasoningEfforts\": " <> maybe "null" (\xs -> "[" <> Text.intercalate ", " (map (jsonString . renderThinkingLevel) xs) <> "]") (facts ^. #supportedReasoningEfforts),     "      },"   ]
gen/GenModels.hs view
@@ -36,6 +36,7 @@ import GenModelsCore   ( checkAnthropicCompat,     checkIdentifierCollisions,+    checkNativeCacheCompat,     flattenEntries,     renderModule,   )@@ -60,6 +61,9 @@     Left err -> die (Text.unpack err)     Right () -> pure ()   case checkAnthropicCompat allEntries of+    Left err -> die (Text.unpack err)+    Right () -> pure ()+  case checkNativeCacheCompat allEntries of     Left err -> die (Text.unpack err)     Right () -> pure ()   let sorted = sortOn fst allEntries
gen/GenModelsCore.hs view
@@ -15,6 +15,7 @@     flattenEntries,     checkIdentifierCollisions,     checkAnthropicCompat,+    checkNativeCacheCompat,     parseAnthropicThinkingStyle,     sanitizeIdentifier,     renderModule,@@ -30,14 +31,17 @@         supportsForcedToolChoice,         supportsLongCacheRetention,         supportsSamplingParameters,+        supportsThinkingDisabled,         thinkingStyle       ),     AnthropicThinkingStyle (..),     CacheControlFormat (..),     MaxTokensField (..),+    NativeCachePolicy (..),     OpenAICompletionsCompat       ( cacheControlFormat,         maxTokensField,+        nativeCachePolicy,         requiresThinkingAsText,         supportedReasoningEfforts,         supportsLongCacheRetention,@@ -52,6 +56,7 @@     defaultAnthropicMessagesCompat,     defaultOpenAICompletionsCompat,     defaultOpenAIResponsesCompat,+    validateNativeCachePolicy,   ) import Baikai.Model (InputModality (..)) import Baikai.Model qualified as Model@@ -123,8 +128,9 @@     _ -> pure ()   sampling <- o .:? "supportsSamplingParameters" .!= d.supportsSamplingParameters   long <- o .:? "supportsLongCacheRetention" .!= d.supportsLongCacheRetention+  native <- o .:? "nativeCachePolicy"   modern <- o .:? "supportsPromptCacheOptions" .!= d.supportsPromptCacheOptions-  pure d {supportedReasoningEfforts = efforts, supportsSamplingParameters = sampling, supportsLongCacheRetention = long, supportsPromptCacheOptions = modern}+  pure d {nativeCachePolicy = native, supportedReasoningEfforts = efforts, supportsSamplingParameters = sampling, supportsLongCacheRetention = long, supportsPromptCacheOptions = modern}  parseOpenAICompat :: Aeson.Object -> Parser OpenAICompletionsCompat parseOpenAICompat o = do@@ -133,6 +139,7 @@   sst <- o .:? "supportsStrictMode" .!= supportsStrictMode d   rtat <- o .:? "requiresThinkingAsText" .!= requiresThinkingAsText d   tf <- optionalField o "thinkingFormat" parseThinkingFormat (thinkingFormat d)+  native <- o .:? "nativeCachePolicy"   ccf <- optionalMaybeField o "cacheControlFormat" parseCacheControlFormat (cacheControlFormat d)   sus <- o .:? "supportsUsageInStreaming" .!= d.supportsUsageInStreaming   slcr <- o .:? "supportsLongCacheRetention" .!= d.supportsLongCacheRetention@@ -145,7 +152,8 @@     _ -> pure ()   pure     d-      { supportsToolCalls = stc,+      { nativeCachePolicy = native,+        supportsToolCalls = stc,         supportsSamplingParameters = ssp,         supportedReasoningEfforts = efforts,         maxTokensField = mtf,@@ -167,6 +175,7 @@   ssp <- o .:? "supportsSamplingParameters" .!= d.supportsSamplingParameters   fast <- o .:? "supportsFastMode" .!= False   forced <- o .:? "supportsForcedToolChoice" .!= d.supportsForcedToolChoice+  disabled <- o .:? "supportsThinkingDisabled" .!= d.supportsThinkingDisabled   pure     d       { supportsLongCacheRetention = slcr,@@ -175,7 +184,8 @@         thinkingStyle = ts,         supportsSamplingParameters = ssp,         supportsFastMode = fast,-        supportsForcedToolChoice = forced+        supportsForcedToolChoice = forced,+        supportsThinkingDisabled = disabled       }  -- | The catalog dialect spells the extended-thinking wire shape as a@@ -424,6 +434,26 @@         <> ",\"thinkingStyle\":\"budget\"|\"adaptive\""         <> ",\"supportsSamplingParameters\":true|false}" +-- | Validate native endpoint facts and modern Long pricing together.+checkNativeCacheCompat :: [(Text, GeneratedEntry)] -> Either Text ()+checkNativeCacheCompat entries = mapM_ checkNative (map snd entries)+  where+    checkNative e = do+      let (format, policy) = case e.compat of+            CatalogCompatOpenAI c -> (c.cacheControlFormat, c.nativeCachePolicy)+            CatalogCompatResponses c -> (Nothing, c.nativeCachePolicy)+            _ -> (Nothing, Nothing)+          named = either (Left . ((e.modelId <> ": ") <>)) Right+      named (validateNativeCachePolicy format policy)+      case policy of+        Just (NativeCacheModern True _) -> named $ case e.pricingPolicy of+          Just p+            | Just _ <- p.longCacheWriteCost,+              all (\t -> t.longCacheWriteCost /= Nothing) (Model.inputTiers p) ->+                Right ()+          _ -> Left "modern Long requires base and every tier long cache-write rates"+        _ -> Right ()+ -- | Replace any non-identifier character with @_@. Haskell allows -- letters, digits, underscore, and apostrophe; everything else -- (slash, dash, dot, colon, ...) becomes an underscore.@@ -474,13 +504,16 @@         "        supportsForcedToolChoice,",         "        supportsLongCacheRetention,",         "        supportsSamplingParameters,",+        "        supportsThinkingDisabled,",         "        thinkingStyle",         "      ),",         "    AnthropicThinkingStyle (..),",         "    CacheControlFormat (..),",+        "    NativeCachePolicy (..),",         "    MaxTokensField (..),",         "    OpenAICompletionsCompat",-        "      ( cacheControlFormat,",+        "      ( nativeCachePolicy,",+        "        cacheControlFormat,",         "        maxTokensField,",         "        requiresThinkingAsText,",         "        supportedReasoningEfforts,",@@ -648,7 +681,8 @@       "              supportsSamplingParameters = " <> renderBool c.supportsSamplingParameters <> ",",       "              supportedReasoningEfforts = " <> maybe "Nothing" (\xs -> "Just [" <> Text.intercalate ", " (map (Text.pack . show) xs) <> "]") c.supportedReasoningEfforts <> ",",       "              supportsUsageInStreaming = " <> renderBool c.supportsUsageInStreaming <> ",",-      "              supportsLongCacheRetention = " <> renderBool c.supportsLongCacheRetention,+      "              supportsLongCacheRetention = " <> renderBool c.supportsLongCacheRetention <> ",",+      "              nativeCachePolicy = " <> renderNativeCache c.nativeCachePolicy,       "            }"     ]   CatalogCompatResponses c ->@@ -658,7 +692,8 @@       "            { supportedReasoningEfforts = " <> maybe "Nothing" (\xs -> "Just [" <> Text.intercalate ", " (map (Text.pack . show) xs) <> "]") c.supportedReasoningEfforts <> ",",       "              supportsSamplingParameters = " <> renderBool c.supportsSamplingParameters <> ",",       "              supportsLongCacheRetention = " <> renderBool c.supportsLongCacheRetention <> ",",-      "              supportsPromptCacheOptions = " <> renderBool c.supportsPromptCacheOptions,+      "              supportsPromptCacheOptions = " <> renderBool c.supportsPromptCacheOptions <> ",",+      "              nativeCachePolicy = " <> renderNativeCache c.nativeCachePolicy,       "            }"     ]   CatalogCompatAnthropic c ->@@ -671,7 +706,8 @@       "              thinkingStyle = " <> renderAnthropicThinkingStyle c.thinkingStyle <> ",",       "              supportsSamplingParameters = " <> renderBool c.supportsSamplingParameters <> ",",       "              supportsFastMode = " <> renderBool c.supportsFastMode <> ",",-      "              supportsForcedToolChoice = " <> renderBool c.supportsForcedToolChoice,+      "              supportsForcedToolChoice = " <> renderBool c.supportsForcedToolChoice <> ",",+      "              supportsThinkingDisabled = " <> renderBool c.supportsThinkingDisabled,       "            }"     ] @@ -715,3 +751,7 @@       "              }",       "          ),"     ]++renderNativeCache :: Maybe NativeCachePolicy -> Text+renderNativeCache Nothing = "Nothing"+renderNativeCache (Just p) = "Just (" <> Text.pack (show p) <> ")"
src/Baikai/Compat.hs view
@@ -33,6 +33,7 @@         requiresThinkingAsText,         thinkingFormat,         cacheControlFormat,+        nativeCachePolicy,         supportsToolCalls,         supportsSamplingParameters,         supportedReasoningEfforts,@@ -43,9 +44,11 @@     MaxTokensField (..),     ThinkingFormat (..),     CacheControlFormat (..),+    NativeCachePolicy (..),+    validateNativeCachePolicy,      -- * OpenAI Responses compat-    OpenAIResponsesCompat (supportedReasoningEfforts, supportsSamplingParameters, supportsLongCacheRetention, supportsPromptCacheOptions),+    OpenAIResponsesCompat (supportedReasoningEfforts, supportsSamplingParameters, supportsLongCacheRetention, supportsPromptCacheOptions, nativeCachePolicy),     defaultOpenAIResponsesCompat,      -- * Anthropic Messages compat@@ -56,6 +59,7 @@         thinkingStyle,         supportsFastMode,         supportsForcedToolChoice,+        supportsThinkingDisabled,         supportsSamplingParameters       ),     AnthropicThinkingStyle (..),@@ -132,6 +136,25 @@   deriving stock (Eq, Show, Generic)   deriving anyclass (FromJSON, ToJSON) +-- | Endpoint-specific native OpenAI caching. Absence on a compat record+-- preserves its legacy behavior; explicit unsupported overrides old flags.+-- Legacy arguments declare in_memory and 24h independently. Modern arguments+-- declare 12h and prefix breakpoints; modern always supports the 30m default.+-- The sum prevents mixing legacy retention with modern breakpoints.+data NativeCachePolicy+  = NativeCacheUnsupported+  | NativeCacheLegacy !Bool !Bool+  | NativeCacheModern !Bool !Bool+  deriving stock (Eq, Show, Generic)+  deriving anyclass (FromJSON, ToJSON)++-- | Reject contradictory caller-built as well as curated endpoint facts.+validateNativeCachePolicy :: Maybe CacheControlFormat -> Maybe NativeCachePolicy -> Either Text ()+validateNativeCachePolicy format policy = case (format, policy) of+  (Just _, Just _) -> Left "native and Anthropic cache controls cannot share a binding"+  (_, Just (NativeCacheLegacy False False)) -> Left "legacy cache policy must accept at least one retention value"+  _ -> Right ()+ -- | Which request shape an Anthropic-compatible host/model accepts -- for extended thinking. Budget-era models take -- @{"type":"enabled","budget_tokens":N}@; adaptive-era models take@@ -194,10 +217,11 @@     --   Anthropic-style cache_control marker. Consumed by     --   @Baikai.Provider.OpenAI.Shape.injectCacheControl@ when     --   'cacheControlFormat' is 'Just CacheControlFormatAnthropic'.-    supportsLongCacheRetention :: !Bool+    supportsLongCacheRetention :: !Bool,+    nativeCachePolicy :: !(Maybe NativeCachePolicy)   }   deriving stock (Eq, Show, Generic)-  deriving anyclass (FromJSON, ToJSON)+  deriving anyclass (ToJSON)  -- | OpenAI's own host: every flag at the OpenAI default. defaultOpenAICompletionsCompat :: OpenAICompletionsCompat@@ -212,7 +236,8 @@       supportsToolCalls = True,       supportsSamplingParameters = True,       supportedReasoningEfforts = Nothing,-      supportsLongCacheRetention = True+      supportsLongCacheRetention = True,+      nativeCachePolicy = Nothing     }  -- | Model and endpoint facts for native Responses. This is separate@@ -222,10 +247,11 @@     supportsSamplingParameters :: !Bool,     supportsLongCacheRetention :: !Bool,     -- | Use prompt_cache_options rather than legacy prompt_cache_retention.-    supportsPromptCacheOptions :: !Bool+    supportsPromptCacheOptions :: !Bool,+    nativeCachePolicy :: !(Maybe NativeCachePolicy)   }   deriving stock (Eq, Show, Generic)-  deriving anyclass (FromJSON, ToJSON)+  deriving anyclass (ToJSON)  defaultOpenAIResponsesCompat :: OpenAIResponsesCompat defaultOpenAIResponsesCompat =@@ -233,9 +259,35 @@     { supportedReasoningEfforts = Nothing,       supportsSamplingParameters = True,       supportsLongCacheRetention = True,-      supportsPromptCacheOptions = False+      supportsPromptCacheOptions = False,+      nativeCachePolicy = Nothing     } +-- New policy fields are optional in persisted compatibility JSON.+instance FromJSON OpenAICompletionsCompat where+  parseJSON = withObject "OpenAICompletionsCompat" $ \o ->+    OpenAICompletionsCompat+      <$> o .: "maxTokensField"+      <*> o .: "supportsStrictMode"+      <*> o .: "requiresThinkingAsText"+      <*> o .: "thinkingFormat"+      <*> o .:? "cacheControlFormat"+      <*> o .: "supportsUsageInStreaming"+      <*> o .: "supportsToolCalls"+      <*> o .: "supportsSamplingParameters"+      <*> o .:? "supportedReasoningEfforts"+      <*> o .: "supportsLongCacheRetention"+      <*> o .:? "nativeCachePolicy"++instance FromJSON OpenAIResponsesCompat where+  parseJSON = withObject "OpenAIResponsesCompat" $ \o ->+    OpenAIResponsesCompat+      <$> o .:? "supportedReasoningEfforts"+      <*> o .: "supportsSamplingParameters"+      <*> o .: "supportsLongCacheRetention"+      <*> o .: "supportsPromptCacheOptions"+      <*> o .:? "nativeCachePolicy"+ -- | Feature flags for one Anthropic Messages-compatible host. data AnthropicMessagesCompat = AnthropicMessagesCompat   { -- | Whether the host honours Anthropic's@@ -243,8 +295,9 @@     --   long-retention preferences silently downgrade to ephemeral.     --   Consumed by @Baikai.Provider.Claude.Internal.Request.computeCacheControl@     --   for top-level cache markers and by-    --   @Baikai.Provider.Claude.Shape.injectToolCacheControl@ for-    --   tool cache markers.+    --   @Baikai.Provider.Claude.Shape.injectToolCacheControl@ and+    --   @Baikai.Provider.Claude.Shape.injectSystemCacheControl@ for+    --   tool and system-prompt cache markers.     supportsLongCacheRetention :: !Bool,     -- | Whether tool definitions accept @cache_control@ markers.     --   Consumed by@@ -276,7 +329,15 @@     supportsFastMode :: !Bool,     -- | Whether this generation accepts required or named tool choice.     -- Defaults to True; explicit catalog facts disable unsupported choices.-    supportsForcedToolChoice :: !Bool+    supportsForcedToolChoice :: !Bool,+    -- | Whether this generation accepts @thinking: {"type": "disabled"}@,+    -- which 'Baikai.ThinkingLevel.ThinkingNone' sends. Defaults to False:+    -- unannotated and legacy records do not establish disable support.+    -- Claude Opus 5.5, Sonnet 5.5, Fable 5 and Fable 5.1 reject it with a 400, and their catalog+    -- records say so; there @none@ clamps to the lowest adaptive effort+    -- and the clamp is recorded. Consumed by+    -- @Baikai.Provider.Claude.Internal.Request.computeThinking@.+    supportsThinkingDisabled :: !Bool   }   deriving stock (Eq, Show, Generic)   deriving anyclass (ToJSON)@@ -291,10 +352,12 @@       thinkingStyle = AnthropicThinkingBudget,       supportsSamplingParameters = True,       supportsFastMode = False,-      supportsForcedToolChoice = True+      supportsForcedToolChoice = True,+      supportsThinkingDisabled = False     } --- | Older persisted models predate fast-mode and forced-choice capabilities.+-- | Older persisted models predate the fast-mode, forced-choice and+-- disabled-thinking capabilities. instance FromJSON AnthropicMessagesCompat where   parseJSON = withObject "AnthropicMessagesCompat" $ \o ->     AnthropicMessagesCompat@@ -305,6 +368,7 @@       <*> o .: "supportsSamplingParameters"       <*> o .:? "supportsFastMode" .!= False       <*> o .:? "supportsForcedToolChoice" .!= True+      <*> o .:? "supportsThinkingDisabled" .!= False  -- | Pick a sensible compat record for an unknown OpenAI-compatible -- host based on its @baseUrl@. Falls back to
src/Baikai/Evidence.hs view
@@ -320,12 +320,16 @@ -- the requested thinking level; it must not refuse one merely because -- a sampling parameter had nowhere to go. The six level-carrying -- constructors weaken thinking; the sampling and speed ones do not.+--+-- A model catalogued as non-reasoning already satisfies ThinkingNone.+-- A host without controls may still reason by default: dropping none+-- there cannot satisfy a strict caller. weakensThinking :: ThinkingAdjustment -> Bool weakensThinking = \case   EffortClamped {} -> True   EffortCollapsedToToggle {} -> True   EffortOmitted {} -> True-  ThinkingDroppedUnsupportedModel {} -> True+  ThinkingDroppedUnsupportedModel lvl -> lvl /= ThinkingNone   ThinkingDroppedUnsupportedHost {} -> True   ThinkingDroppedBudgetExceeded {} -> True   SamplingDroppedUnsupportedModel {} -> False@@ -919,7 +923,11 @@ -- Older error objects decode with Nothing; digest inputs are unchanged. -- Version 2.6 adds structured response content and optional request_problem. -- The canonical encoding and data-free commitment fixtures are unchanged.-evidenceSchemaVersion = "baikai.model-call-evidence/2.6"+-- Version 2.7 adds the explicit none thinking preference. Existing digests and+-- observations are unchanged; a transmitted off request remains unobserved.+-- Version 2.8 projects native cache controls and content-free Responses input+-- structure. Envelopes lacking the new fields preserve their prior digests.+evidenceSchemaVersion = "baikai.model-call-evidence/2.8"  -- | Everything Baikai can say about one completed provider call. --@@ -1270,6 +1278,10 @@   where     keep (k, v) = case Key.toText k of       "messages" -> [(k, summariseMessages v)]+      "input" -> [(k, summariseInput v)]+      "instructions" -> [(k, charSummary v)]+      "prompt_cache_options" -> [(k, summariseNativeControl v)]+      "prompt_cache_retention" -> [(k, allowedString ["in_memory", "24h"] v)]       "system" -> [(k, charSummary v)]       "tools" -> [(k, summariseTools v)]       "output_config" -> [(k, summariseOutputConfig v)]@@ -1312,10 +1324,12 @@ summariseMessage = \case   Object m ->     object-      [ "role" .= roleOf (KeyMap.lookup "role" m),-        "blocks" .= blockCount (KeyMap.lookup "content" m),-        "chars" .= maybe 0 totalStringChars (KeyMap.lookup "content" m)-      ]+      ( [ "role" .= roleOf (KeyMap.lookup "role" m),+          "blocks" .= blockCount (KeyMap.lookup "content" m),+          "chars" .= maybe 0 totalStringChars (KeyMap.lookup "content" m)+        ]+          <> markerSummary (KeyMap.lookup "content" m)+      )   _ -> Null   where     roleOf = \case@@ -1327,6 +1341,44 @@       Just Null -> 0       Nothing -> 0       Just _ -> 1++-- Only native policy vocabulary is configuration. Unknown nested fields,+-- cache identities, text, replay IDs and encrypted state never enter it.+allowedString :: [Text] -> Value -> Value+allowedString values v@(String t) | t `elem` values = v+allowedString _ _ = Null++summariseNativeControl :: Value -> Value+summariseNativeControl (Object o) =+  Object+    ( KeyMap.fromList+        [(k, allowedString values v) | (k, values) <- [("mode", ["implicit", "explicit"]), ("ttl", ["30m", "12h"])], Just v <- [KeyMap.lookup k o]]+    )+summariseNativeControl _ = Null++markerSummary :: Maybe Value -> [(Key.Key, Value)]+markerSummary (Just (Array parts)) =+  let markers =+        [ object ["index" .= i, "control" .= summariseNativeControl marker]+        | (i, Object part) <- zip [0 :: Int ..] (Vector.toList parts),+          Just marker <- [KeyMap.lookup "prompt_cache_breakpoint" part]+        ]+   in [("cache_breakpoints", toJSON markers) | not (null markers)]+markerSummary _ = []++summariseInput :: Value -> Value+summariseInput (Array items) = toJSON (map item (Vector.toList items))+  where+    item (Object o) =+      object+        ( [ "role" .= maybe Null (allowedString ["system", "developer", "user", "assistant"]) (KeyMap.lookup "role" o),+            "type" .= maybe Null (allowedString ["message", "reasoning", "function_call", "function_call_output"]) (KeyMap.lookup "type" o),+            "chars" .= totalStringChars (Object o)+          ]+            <> markerSummary (KeyMap.lookup "content" o)+        )+    item v = charSummary v+summariseInput v = charSummary v  -- | Total characters across every JSON string anywhere inside a value. -- Recursive on purpose: a content block's text can sit at any depth,
src/Baikai/Models/Generated.hs view
@@ -15,13 +15,16 @@         supportsForcedToolChoice,         supportsLongCacheRetention,         supportsSamplingParameters,+        supportsThinkingDisabled,         thinkingStyle       ),     AnthropicThinkingStyle (..),     CacheControlFormat (..),+    NativeCachePolicy (..),     MaxTokensField (..),     OpenAICompletionsCompat-      ( cacheControlFormat,+      ( nativeCachePolicy,+        cacheControlFormat,         maxTokensField,         requiresThinkingAsText,         supportedReasoningEfforts,@@ -99,7 +102,8 @@               thinkingStyle = AnthropicThinkingAdaptive,               supportsSamplingParameters = False,               supportsFastMode = False,-              supportsForcedToolChoice = True+              supportsForcedToolChoice = True,+              supportsThinkingDisabled = False             }     } @@ -135,7 +139,8 @@               thinkingStyle = AnthropicThinkingAdaptive,               supportsSamplingParameters = False,               supportsFastMode = False,-              supportsForcedToolChoice = False+              supportsForcedToolChoice = False,+              supportsThinkingDisabled = False             }     } @@ -171,7 +176,8 @@               thinkingStyle = AnthropicThinkingBudget,               supportsSamplingParameters = True,               supportsFastMode = False,-              supportsForcedToolChoice = True+              supportsForcedToolChoice = True,+              supportsThinkingDisabled = True             }     } @@ -207,7 +213,8 @@               thinkingStyle = AnthropicThinkingAdaptive,               supportsSamplingParameters = False,               supportsFastMode = False,-              supportsForcedToolChoice = True+              supportsForcedToolChoice = True,+              supportsThinkingDisabled = True             }     } @@ -243,7 +250,8 @@               thinkingStyle = AnthropicThinkingBudget,               supportsSamplingParameters = True,               supportsFastMode = False,-              supportsForcedToolChoice = True+              supportsForcedToolChoice = True,+              supportsThinkingDisabled = True             }     } @@ -279,7 +287,8 @@               thinkingStyle = AnthropicThinkingAdaptive,               supportsSamplingParameters = True,               supportsFastMode = False,-              supportsForcedToolChoice = True+              supportsForcedToolChoice = True,+              supportsThinkingDisabled = True             }     } @@ -315,7 +324,8 @@               thinkingStyle = AnthropicThinkingAdaptive,               supportsSamplingParameters = False,               supportsFastMode = False,-              supportsForcedToolChoice = True+              supportsForcedToolChoice = True,+              supportsThinkingDisabled = True             }     } @@ -359,7 +369,8 @@               thinkingStyle = AnthropicThinkingAdaptive,               supportsSamplingParameters = False,               supportsFastMode = True,-              supportsForcedToolChoice = True+              supportsForcedToolChoice = True,+              supportsThinkingDisabled = True             }     } @@ -403,7 +414,8 @@               thinkingStyle = AnthropicThinkingAdaptive,               supportsSamplingParameters = False,               supportsFastMode = True,-              supportsForcedToolChoice = True+              supportsForcedToolChoice = True,+              supportsThinkingDisabled = True             }     } @@ -447,7 +459,8 @@               thinkingStyle = AnthropicThinkingAdaptive,               supportsSamplingParameters = False,               supportsFastMode = True,-              supportsForcedToolChoice = False+              supportsForcedToolChoice = False,+              supportsThinkingDisabled = False             }     } @@ -483,7 +496,8 @@               thinkingStyle = AnthropicThinkingBudget,               supportsSamplingParameters = True,               supportsFastMode = False,-              supportsForcedToolChoice = True+              supportsForcedToolChoice = True,+              supportsThinkingDisabled = True             }     } @@ -519,7 +533,8 @@               thinkingStyle = AnthropicThinkingAdaptive,               supportsSamplingParameters = True,               supportsFastMode = False,-              supportsForcedToolChoice = True+              supportsForcedToolChoice = True,+              supportsThinkingDisabled = True             }     } @@ -555,7 +570,8 @@               thinkingStyle = AnthropicThinkingAdaptive,               supportsSamplingParameters = False,               supportsFastMode = False,-              supportsForcedToolChoice = True+              supportsForcedToolChoice = True,+              supportsThinkingDisabled = True             }     } @@ -591,7 +607,8 @@               thinkingStyle = AnthropicThinkingAdaptive,               supportsSamplingParameters = False,               supportsFastMode = False,-              supportsForcedToolChoice = False+              supportsForcedToolChoice = False,+              supportsThinkingDisabled = False             }     } @@ -670,7 +687,21 @@       contextWindow = 1047576,       maxOutputTokens = 32768,       headers = Map.empty,-      compat = CompatNone+      compat =+        CompatOpenAICompletions+          defaultOpenAICompletionsCompat+            { maxTokensField = MaxCompletionTokensField,+              supportsStrictMode = True,+              requiresThinkingAsText = False,+              thinkingFormat = ThinkingFormatOpenAI,+              cacheControlFormat = Nothing,+              supportsToolCalls = True,+              supportsSamplingParameters = True,+              supportedReasoningEfforts = Nothing,+              supportsUsageInStreaming = True,+              supportsLongCacheRetention = True,+              nativeCachePolicy = Just (NativeCacheLegacy True True)+            }     }  openai_gpt_4_1_mini :: Model@@ -800,7 +831,21 @@       contextWindow = 400000,       maxOutputTokens = 128000,       headers = Map.empty,-      compat = CompatNone+      compat =+        CompatOpenAICompletions+          defaultOpenAICompletionsCompat+            { maxTokensField = MaxCompletionTokensField,+              supportsStrictMode = True,+              requiresThinkingAsText = False,+              thinkingFormat = ThinkingFormatOpenAI,+              cacheControlFormat = Nothing,+              supportsToolCalls = True,+              supportsSamplingParameters = True,+              supportedReasoningEfforts = Just [ThinkingMinimal, ThinkingLow, ThinkingMedium, ThinkingHigh],+              supportsUsageInStreaming = True,+              supportsLongCacheRetention = True,+              nativeCachePolicy = Nothing+            }     }  openai_gpt_5_1 :: Model@@ -956,7 +1001,21 @@       contextWindow = 1050000,       maxOutputTokens = 128000,       headers = Map.empty,-      compat = CompatNone+      compat =+        CompatOpenAICompletions+          defaultOpenAICompletionsCompat+            { maxTokensField = MaxCompletionTokensField,+              supportsStrictMode = True,+              requiresThinkingAsText = False,+              thinkingFormat = ThinkingFormatOpenAI,+              cacheControlFormat = Nothing,+              supportsToolCalls = True,+              supportsSamplingParameters = True,+              supportedReasoningEfforts = Nothing,+              supportsUsageInStreaming = True,+              supportsLongCacheRetention = True,+              nativeCachePolicy = Just (NativeCacheLegacy False True)+            }     }  openai_gpt_5_6 :: Model@@ -982,7 +1041,21 @@       contextWindow = 1050000,       maxOutputTokens = 128000,       headers = Map.empty,-      compat = CompatNone+      compat =+        CompatOpenAICompletions+          defaultOpenAICompletionsCompat+            { maxTokensField = MaxCompletionTokensField,+              supportsStrictMode = True,+              requiresThinkingAsText = False,+              thinkingFormat = ThinkingFormatOpenAI,+              cacheControlFormat = Nothing,+              supportsToolCalls = True,+              supportsSamplingParameters = True,+              supportedReasoningEfforts = Nothing,+              supportsUsageInStreaming = True,+              supportsLongCacheRetention = True,+              nativeCachePolicy = Just (NativeCacheModern False True)+            }     }  openai_gpt_5_6_luna :: Model@@ -1086,7 +1159,21 @@       contextWindow = 400000,       maxOutputTokens = 128000,       headers = Map.empty,-      compat = CompatNone+      compat =+        CompatOpenAICompletions+          defaultOpenAICompletionsCompat+            { maxTokensField = MaxCompletionTokensField,+              supportsStrictMode = True,+              requiresThinkingAsText = False,+              thinkingFormat = ThinkingFormatOpenAI,+              cacheControlFormat = Nothing,+              supportsToolCalls = True,+              supportsSamplingParameters = True,+              supportedReasoningEfforts = Just [ThinkingMinimal, ThinkingLow, ThinkingMedium, ThinkingHigh],+              supportsUsageInStreaming = True,+              supportsLongCacheRetention = True,+              nativeCachePolicy = Nothing+            }     }  openai_gpt_5_nano :: Model@@ -1112,7 +1199,21 @@       contextWindow = 400000,       maxOutputTokens = 128000,       headers = Map.empty,-      compat = CompatNone+      compat =+        CompatOpenAICompletions+          defaultOpenAICompletionsCompat+            { maxTokensField = MaxCompletionTokensField,+              supportsStrictMode = True,+              requiresThinkingAsText = False,+              thinkingFormat = ThinkingFormatOpenAI,+              cacheControlFormat = Nothing,+              supportsToolCalls = True,+              supportsSamplingParameters = True,+              supportedReasoningEfforts = Just [ThinkingMinimal, ThinkingLow, ThinkingMedium, ThinkingHigh],+              supportsUsageInStreaming = True,+              supportsLongCacheRetention = True,+              nativeCachePolicy = Nothing+            }     }  openai_gpt_6_1_sol :: Model@@ -1144,7 +1245,8 @@             { supportedReasoningEfforts = Just [ThinkingLow, ThinkingMedium, ThinkingHigh, ThinkingXHigh, ThinkingMax],               supportsSamplingParameters = False,               supportsLongCacheRetention = False,-              supportsPromptCacheOptions = True+              supportsPromptCacheOptions = True,+              nativeCachePolicy = Just (NativeCacheModern False True)             }     } @@ -1177,7 +1279,8 @@             { supportedReasoningEfforts = Just [ThinkingLow, ThinkingMedium, ThinkingHigh, ThinkingXHigh, ThinkingMax],               supportsSamplingParameters = False,               supportsLongCacheRetention = False,-              supportsPromptCacheOptions = True+              supportsPromptCacheOptions = True,+              nativeCachePolicy = Just (NativeCacheModern False True)             }     } @@ -1200,17 +1303,18 @@             cacheWriteCost = 1 % 8           },       fastModeCost = Nothing,-      pricingPolicy = Just (PricingPolicy [InputPriceTier 272000 (ModelCost (1 % 5) (3 % 4) (1 % 50) (1 % 4)) Nothing] Nothing),+      pricingPolicy = Just (PricingPolicy [InputPriceTier 272000 (ModelCost (1 % 5) (3 % 4) (1 % 50) (1 % 4)) (Just (1 % 2))] (Just (1 % 4))),       contextWindow = 1050000,       maxOutputTokens = 128000,       headers = Map.empty,       compat =         CompatOpenAIResponses           defaultOpenAIResponsesCompat-            { supportedReasoningEfforts = Just [ThinkingLow, ThinkingMedium, ThinkingHigh, ThinkingXHigh, ThinkingMax],+            { supportedReasoningEfforts = Just [ThinkingNone, ThinkingLow, ThinkingMedium, ThinkingHigh, ThinkingXHigh, ThinkingMax],               supportsSamplingParameters = False,-              supportsLongCacheRetention = False,-              supportsPromptCacheOptions = True+              supportsLongCacheRetention = True,+              supportsPromptCacheOptions = True,+              nativeCachePolicy = Just (NativeCacheModern True True)             }     } @@ -1240,10 +1344,11 @@       compat =         CompatOpenAIResponses           defaultOpenAIResponsesCompat-            { supportedReasoningEfforts = Just [ThinkingLow, ThinkingMedium, ThinkingHigh, ThinkingXHigh, ThinkingMax],+            { supportedReasoningEfforts = Just [ThinkingNone, ThinkingLow, ThinkingMedium, ThinkingHigh, ThinkingXHigh, ThinkingMax],               supportsSamplingParameters = False,               supportsLongCacheRetention = False,-              supportsPromptCacheOptions = True+              supportsPromptCacheOptions = True,+              nativeCachePolicy = Just (NativeCacheModern False True)             }     } @@ -1270,7 +1375,21 @@       contextWindow = 200000,       maxOutputTokens = 100000,       headers = Map.empty,-      compat = CompatNone+      compat =+        CompatOpenAICompletions+          defaultOpenAICompletionsCompat+            { maxTokensField = MaxCompletionTokensField,+              supportsStrictMode = True,+              requiresThinkingAsText = False,+              thinkingFormat = ThinkingFormatOpenAI,+              cacheControlFormat = Nothing,+              supportsToolCalls = True,+              supportsSamplingParameters = True,+              supportedReasoningEfforts = Just [ThinkingLow, ThinkingMedium, ThinkingHigh],+              supportsUsageInStreaming = True,+              supportsLongCacheRetention = True,+              nativeCachePolicy = Nothing+            }     }  openai_o3 :: Model@@ -1296,7 +1415,21 @@       contextWindow = 200000,       maxOutputTokens = 100000,       headers = Map.empty,-      compat = CompatNone+      compat =+        CompatOpenAICompletions+          defaultOpenAICompletionsCompat+            { maxTokensField = MaxCompletionTokensField,+              supportsStrictMode = True,+              requiresThinkingAsText = False,+              thinkingFormat = ThinkingFormatOpenAI,+              cacheControlFormat = Nothing,+              supportsToolCalls = True,+              supportsSamplingParameters = True,+              supportedReasoningEfforts = Just [ThinkingLow, ThinkingMedium, ThinkingHigh],+              supportsUsageInStreaming = True,+              supportsLongCacheRetention = True,+              nativeCachePolicy = Nothing+            }     }  openai_o3_mini :: Model@@ -1322,7 +1455,21 @@       contextWindow = 200000,       maxOutputTokens = 100000,       headers = Map.empty,-      compat = CompatNone+      compat =+        CompatOpenAICompletions+          defaultOpenAICompletionsCompat+            { maxTokensField = MaxCompletionTokensField,+              supportsStrictMode = True,+              requiresThinkingAsText = False,+              thinkingFormat = ThinkingFormatOpenAI,+              cacheControlFormat = Nothing,+              supportsToolCalls = True,+              supportsSamplingParameters = True,+              supportedReasoningEfforts = Just [ThinkingLow, ThinkingMedium, ThinkingHigh],+              supportsUsageInStreaming = True,+              supportsLongCacheRetention = True,+              nativeCachePolicy = Nothing+            }     }  openai_o4_mini :: Model@@ -1348,7 +1495,21 @@       contextWindow = 200000,       maxOutputTokens = 100000,       headers = Map.empty,-      compat = CompatNone+      compat =+        CompatOpenAICompletions+          defaultOpenAICompletionsCompat+            { maxTokensField = MaxCompletionTokensField,+              supportsStrictMode = True,+              requiresThinkingAsText = False,+              thinkingFormat = ThinkingFormatOpenAI,+              cacheControlFormat = Nothing,+              supportsToolCalls = True,+              supportsSamplingParameters = True,+              supportedReasoningEfforts = Just [ThinkingLow, ThinkingMedium, ThinkingHigh],+              supportsUsageInStreaming = True,+              supportsLongCacheRetention = True,+              nativeCachePolicy = Nothing+            }     }  openrouter_anthropic_claude_sonnet_4 :: Model
src/Baikai/Options.hs view
@@ -105,11 +105,21 @@     -- | 'Nothing' and @Just 'CacheRetentionNone'@ are the same request:     -- both send no cache-control marker. The constructor is kept for a     -- caller who wants to say "no caching" explicitly.+    --+    -- On Anthropic Messages hosts a retention sets up to three cache+    -- breakpoints: the top-level automatic marker (the end of the+    -- request), the last tool definition, and the system prompt, which+    -- is then sent as one text block carrying @cache_control@. The+    -- system-prompt breakpoint lets requests that share a static system+    -- prefix but differ in their messages read it from the cache.     cacheRetention :: !(Maybe CacheRetention),     -- | Nothing omits the speed field; standard explicitly requests standard     -- speed. Fast is catalog-gated by Anthropic and dropped with evidence on     -- unsupported models. See "Baikai.Speed" for availability and pricing.     speed :: !(Maybe Speed),+    -- | Nothing leaves the provider default; Just ThinkingNone requests off.+    -- Unsupported requests are recorded as adjustments; strict evidence refuses+    -- a fallback before dispatch. Translation is not proof of server execution.     thinking :: !(Maybe ThinkingLevel),     responseFormat :: !(Maybe ResponseFormat),     evidence :: !(Maybe EvidenceRequest),
src/Baikai/ThinkingLevel.hs view
@@ -3,7 +3,7 @@ -- | Provider-agnostic reasoning-effort preference. -- -- Each provider maps the value to its own primitive. The canonical--- rendering spans @minimal@ through @max@, while provider boundaries+-- rendering spans @none@ through @max@, while provider boundaries -- may preserve, translate, or clamp values according to their APIs. -- 'thinkingTokenBudget' provides the recommended budget for -- token-based providers.@@ -25,7 +25,18 @@ -- provider's vocabulary, fine enough to influence behaviour in a -- meaningful way. data ThinkingLevel-  = ThinkingMinimal+  = -- | Ask for no reasoning at all. This is not the same as leaving+    -- 'Baikai.Options.thinking' at 'Nothing': that sends nothing and+    -- leaves the model's own default in force, which on an adaptive+    -- Anthropic generation such as Claude Haiku 5.5 is to think. This+    -- level asks for thinking to be turned off: Anthropic Messages+    -- sends @thinking: {"type": "disabled"}@ to a model whose catalog+    -- record says it accepts that ('Baikai.Compat.supportsThinkingDisabled')+    -- and otherwise records a fallback. OpenAI sends @none@ when the+    -- model's accepted-effort policy permits it. Strict evidence refuses+    -- substitutions before dispatch; no mapping proves server execution.+    ThinkingNone+  | ThinkingMinimal   | ThinkingLow   | ThinkingMedium   | ThinkingHigh@@ -37,6 +48,7 @@ -- | Render the canonical Baikai name for a reasoning-effort level. renderThinkingLevel :: ThinkingLevel -> Text renderThinkingLevel = \case+  ThinkingNone -> "none"   ThinkingMinimal -> "minimal"   ThinkingLow -> "low"   ThinkingMedium -> "medium"@@ -51,6 +63,7 @@ -- @Baikai.Agent.Cli@ — did the first time a level was added. parseThinkingLevel :: Text -> Maybe ThinkingLevel parseThinkingLevel = \case+  "none" -> Just ThinkingNone   "minimal" -> Just ThinkingMinimal   "low" -> Just ThinkingLow   "medium" -> Just ThinkingMedium@@ -60,9 +73,11 @@   _ -> Nothing  -- | Recommended token budget for providers that take an explicit--- count (Anthropic's @thinking.budget_tokens@).+-- count (Anthropic's @thinking.budget_tokens@). 'ThinkingNone' asks+-- for no budget, so it is @0@; no provider sends a budget for it. thinkingTokenBudget :: ThinkingLevel -> Natural thinkingTokenBudget = \case+  ThinkingNone -> 0   ThinkingMinimal -> 1024   ThinkingLow -> 2048   ThinkingMedium -> 8192
test/CatalogSpec.hs view
@@ -28,8 +28,10 @@     supportsFastMode,     supportsForcedToolChoice,     supportsSamplingParameters,+    supportsThinkingDisabled,     thinkingStyle,   )+import Baikai.Compat qualified as C import Baikai.Model   ( Compat (CompatAnthropicMessages, CompatOpenAIResponses),     Model,@@ -37,6 +39,7 @@     compat,     modelId,   )+import Baikai.Model qualified as M import Baikai.Models.Generated (allModels) import Baikai.ThinkingLevel (ThinkingLevel (..)) import Data.ByteString qualified as BS@@ -51,7 +54,17 @@ tests =   testGroup     "Baikai.Models.Generated"-    [ testCase "Astra selects Responses with explicit endpoint facts" $ do+    [ testCase "native cache facts enable verified Luna Long and keep Chat gated" $ do+        let findModel mid = case filter ((== mid) . modelId) allModels of [m] -> pure m; _ -> assertFailure "missing model"+        luna <- findModel "gpt-6-luna"+        chat <- findModel "gpt-5.6"+        both <- findModel "gpt-4.1"+        longOnly <- findModel "gpt-5.5"+        (M.openaiResponsesCompatFor luna).nativeCachePolicy @?= Just (C.NativeCacheModern True True)+        (M.openaiCompletionsCompatFor chat).nativeCachePolicy @?= Just (C.NativeCacheModern False True)+        (M.openaiCompletionsCompatFor both).nativeCachePolicy @?= Just (C.NativeCacheLegacy True True)+        (M.openaiCompletionsCompatFor longOnly).nativeCachePolicy @?= Just (C.NativeCacheLegacy False True),+      testCase "Astra selects Responses with explicit endpoint facts" $ do         [api m | m <- allModels, modelId m == "gpt-6-astra"] @?= [OpenAIResponses]         case [compat m | m <- allModels, modelId m == "gpt-6-astra"] of           [CompatOpenAIResponses c] -> do@@ -67,9 +80,9 @@               case [compat m | m <- allModels, modelId m == mid] of                 [CompatOpenAIResponses c] -> do                   c.supportsPromptCacheOptions @?= True-                  c.supportsLongCacheRetention @?= False+                  c.supportsLongCacheRetention @?= (mid == "gpt-6-luna")                   c.supportsSamplingParameters @?= False-                  c.supportedReasoningEfforts @?= Just [ThinkingLow, ThinkingMedium, ThinkingHigh, ThinkingXHigh, ThinkingMax]+                  c.supportedReasoningEfforts @?= Just ([ThinkingNone | mid /= "gpt-6.1-sol"] <> [ThinkingLow, ThinkingMedium, ThinkingHigh, ThinkingXHigh, ThinkingMax])                 _ -> assertFailure "GPT-6 Sol/Luna need explicit OpenAI Responses facts"           )           ["gpt-6-sol", "gpt-6-luna", "gpt-6.1-sol"],@@ -103,25 +116,25 @@ anthropicCatalogModels :: [Model] anthropicCatalogModels = [m | m <- allModels, api m == AnthropicMessages] --- | The shipped thinking style and sampling support of each Anthropic--- catalog id, written out by hand from--- @baikai\/data\/models\/anthropic.json@.-expectedAnthropicFacts :: [(Text, (AnthropicThinkingStyle, Bool, Bool, Bool))]+-- | The shipped thinking style, sampling, forced-choice, fast-mode and+-- disabled-thinking support of each Anthropic catalog id, written out by+-- hand from @baikai\/data\/models\/anthropic.json@.+expectedAnthropicFacts :: [(Text, (AnthropicThinkingStyle, Bool, Bool, Bool, Bool))] expectedAnthropicFacts =-  [ ("claude-fable-5", (AnthropicThinkingAdaptive, False, True, False)),-    ("claude-fable-5-1", (AnthropicThinkingAdaptive, False, False, False)),-    ("claude-haiku-4-5", (AnthropicThinkingBudget, True, True, False)),-    ("claude-haiku-5-5", (AnthropicThinkingAdaptive, False, True, False)),-    ("claude-opus-4-5", (AnthropicThinkingBudget, True, True, False)),-    ("claude-opus-4-6", (AnthropicThinkingAdaptive, True, True, False)),-    ("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)),-    ("claude-sonnet-5-5", (AnthropicThinkingAdaptive, False, False, False))+  [ ("claude-fable-5", (AnthropicThinkingAdaptive, False, True, False, False)),+    ("claude-fable-5-1", (AnthropicThinkingAdaptive, False, False, False, False)),+    ("claude-haiku-4-5", (AnthropicThinkingBudget, True, True, False, True)),+    ("claude-haiku-5-5", (AnthropicThinkingAdaptive, False, True, False, True)),+    ("claude-opus-4-5", (AnthropicThinkingBudget, True, True, False, True)),+    ("claude-opus-4-6", (AnthropicThinkingAdaptive, True, True, False, True)),+    ("claude-opus-4-7", (AnthropicThinkingAdaptive, False, True, False, True)),+    ("claude-opus-4-8", (AnthropicThinkingAdaptive, False, True, True, True)),+    ("claude-opus-5", (AnthropicThinkingAdaptive, False, True, True, True)),+    ("claude-opus-5-5", (AnthropicThinkingAdaptive, False, False, True, False)),+    ("claude-sonnet-4-5", (AnthropicThinkingBudget, True, True, False, True)),+    ("claude-sonnet-4-6", (AnthropicThinkingAdaptive, True, True, False, True)),+    ("claude-sonnet-5", (AnthropicThinkingAdaptive, False, True, False, True)),+    ("claude-sonnet-5-5", (AnthropicThinkingAdaptive, False, False, False, False))   ]  assertFacts :: Model -> IO ()@@ -139,5 +152,5 @@           <> show other       )   where-    facts :: AnthropicMessagesCompat -> (AnthropicThinkingStyle, Bool, Bool, Bool)-    facts c = (thinkingStyle c, c.supportsSamplingParameters, c.supportsForcedToolChoice, c.supportsFastMode)+    facts :: AnthropicMessagesCompat -> (AnthropicThinkingStyle, Bool, Bool, Bool, Bool)+    facts c = (thinkingStyle c, c.supportsSamplingParameters, c.supportsForcedToolChoice, c.supportsFastMode, c.supportsThinkingDisabled)
test/CostSpec.hs view
@@ -47,7 +47,12 @@ tests =   testGroup     "Baikai.Cost"-    [ fastCostTests,+    [ testCase "synthetic native duration rates select the complete input tier once" $ do+        let m = knownModel & #pricingPolicy .~ Just (PricingPolicy [InputPriceTier 100 (ModelCost 2 4 (1 / 5) (5 / 2)) (Just 5)] (Just (5 / 2)))+            u = Usage.observeBilling [Usage.BillingServiceTier "default"] (zeroUsage & #cacheWriteTokens .~ 200 & #totalTokens .~ 200)+        (computeCostForService (Just CacheRetentionShort) Nothing m u) ^. #usd @?= 1 / 2000+        (computeCostForService (Just CacheRetentionLong) Nothing m u) ^. #usd @?= 1 / 1000,+      fastCostTests,       computeTests,       attachCostTests,       callLogTests
test/EvidenceSpec.hs view
@@ -27,7 +27,14 @@ tests =   testGroup     "Evidence"-    [ translationDisplayTests,+    [ testCase "native cache configuration redacts replay and records marker position" $ do+        let marker = object ["mode" .= ("explicit" :: Text.Text), "secret" .= ("PRIVATE_CONTROL" :: Text.Text)]+            block marked = object (["type" .= ("input_text" :: Text.Text), "text" .= ("PRIVATE_TEXT" :: Text.Text)] <> ["prompt_cache_breakpoint" .= marker | marked])+            body marked = object ["prompt_cache_options" .= object ["ttl" .= ("30m" :: Text.Text), "cache_key" .= ("PRIVATE_KEY" :: Text.Text)], "input" .= [object ["role" .= ("developer" :: Text.Text), "content" .= [block marked]], object ["type" .= ("reasoning" :: Text.Text), "encrypted_content" .= ("PRIVATE_REPLAY" :: Text.Text), "id" .= ("PRIVATE_ID" :: Text.Text)]]]+            projection = Text.pack (show (configurationProjection (body True)))+        assertBool "no private content" (not ("PRIVATE" `Text.isInfixOf` projection))+        assertBool "marker placement matters" (configurationDigest (body True) /= configurationDigest (body False)),+      translationDisplayTests,       canonicalTests,       digestTests,       usageEnvelopeTests,@@ -271,7 +278,7 @@   testGroup     "usage envelope"     [ testCase "cost basis is serialized additively without changing provider commitments" $ do-        evidenceSchemaVersion @?= "baikai.model-call-evidence/2.6"+        evidenceSchemaVersion @?= "baikai.model-call-evidence/2.8"         let estimated = zeroUsage {cost = estimateCost [CacheWriteUsageNotReported] zeroCost}         usageEnvelope estimated @?= usageEnvelope zeroUsage         assertBool "usage JSON retains the local calculation basis" (Aeson.toJSON estimated /= Aeson.toJSON zeroUsage),
test/FetchModelsSpec.hs view
@@ -1,14 +1,18 @@+{-# LANGUAGE OverloadedRecordDot #-}+ -- | Unit tests for the pure core of @baikai-fetch-models@ -- ('FetchModelsCore'): filtering, curation, modality mapping, cost -- defaulting, and rendering, exercised against a checked-in -- models.dev-shaped fixture. No network is involved. module FetchModelsSpec (tests) where -import Baikai.Compat (AnthropicThinkingStyle (..), defaultOpenAIResponsesCompat)+import Baikai.Compat (AnthropicThinkingStyle (..), defaultOpenAICompletionsCompat, defaultOpenAIResponsesCompat)+import Baikai.Compat qualified as C import Baikai.Model (InputModality (..)) import Baikai.Model qualified import Baikai.Model qualified as Model import Baikai.Prelude+import Baikai.ThinkingLevel (ThinkingLevel (..)) import Control.Monad (forM_) import Data.Aeson qualified as Aeson import Data.Aeson.KeyMap qualified as KeyMap@@ -67,7 +71,7 @@               contextWindow = 400000,               maxOutputTokens = 128000,               apiOverride = Nothing,-              compat = Nothing+              compat = Just (CatalogOpenAICompat (defaultOpenAICompletionsCompat & #supportedReasoningEfforts .~ Just [ThinkingMinimal, ThinkingLow, ThinkingMedium, ThinkingHigh]))             },           CatalogModel             { modelId = "gpt-5.4",@@ -115,6 +119,7 @@                           { thinkingStyle = AnthropicThinkingBudget,                             supportsSamplingParameters = True,                             supportsForcedToolChoice = True,+                            supportsThinkingDisabled = True,                             fastModeCost = Nothing                           }                       )@@ -127,8 +132,20 @@ tests =   testGroup     "Baikai.FetchModels"-    [ testCase "per-model API and Responses compat survive normalization and rendering" $ do+    [ testCase "curated native cache facts survive fetch rendering and generator parsing" $ do         upstream <- loadUpstream+        let sample = head (Map.elems (Map.findWithDefault Map.empty "openai" upstream))+        forM_ [("gpt-6-luna", C.NativeCacheModern True True), ("gpt-5.6", C.NativeCacheModern False True), ("gpt-4.1", C.NativeCacheLegacy True True), ("gpt-5.5", C.NativeCacheLegacy False True)] $ \(mid, expected) -> do+          let cat = normalizeProvider openaiSpec (Map.singleton mid (sample & #modelId .~ mid & #toolCall .~ True))+          parsed <- either assertFailure pure (Aeson.eitherDecode (BSL.fromStrict (renderCatalog cat)))+          let entries = Gen.flattenEntries parsed+          case map (\(_, e) -> e.compat) entries of+            [Gen.CatalogCompatOpenAI c] -> c ^. #nativeCachePolicy @?= Just expected+            [Gen.CatalogCompatResponses c] -> c ^. #nativeCachePolicy @?= Just expected+            _ -> assertFailure "missing native compat"+          assertBool "generator renders policy" (Text.pack (show expected) `Text.isInfixOf` Gen.renderModule entries),+      testCase "per-model API and Responses compat survive normalization and rendering" $ do+        upstream <- loadUpstream         let spec =               openaiSpec                 & #apiFor@@ -157,6 +174,14 @@               _ -> assertFailure "wrong entries"             _ -> assertFailure "missing models"           _ -> assertFailure "invalid rendered catalog",+      testCase "refresh and generation preserve which OpenAI models accept none" $ do+        upstream <- loadUpstream+        let sample = (upstream Map.! "openai") Map.! "gpt-5.4"+        forM_ [("gpt-5", "ThinkingMinimal"), ("o3", "ThinkingLow"), ("gpt-6-sol", "ThinkingNone"), ("gpt-6-luna", "ThinkingNone"), ("gpt-6-astra", "ThinkingLow"), ("gpt-6.1-sol", "ThinkingLow")] $ \(mid, lowest) -> do+          let refreshed = normalizeProvider openaiSpec (Map.singleton mid (sample & #modelId .~ mid))+          case Aeson.eitherDecode (BSL.fromStrict (renderCatalog refreshed)) of+            Left err -> assertFailure err+            Right catalog -> assertBool (Text.unpack mid <> " preserves its lowest effort") (("supportedReasoningEfforts = Just [" <> lowest <> ",") `Text.isInfixOf` Gen.renderModule (Gen.flattenEntries catalog)),       testCase "refresh preserves Astra endpoint restrictions despite upstream tool support" $ do         upstream <- loadUpstream         let sample = (upstream Map.! "openai") Map.! "gpt-5.4"@@ -185,7 +210,7 @@         forM_           [ (openaiSpec, "gpt-6-astra", Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 20 75 2 25) Nothing] Nothing),             (openaiSpec, "gpt-6-sol", Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 4 15 0.4 5) Nothing] Nothing),-            (openaiSpec, "gpt-6-luna", Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 0.2 0.75 0.02 0.25) Nothing] Nothing),+            (openaiSpec, "gpt-6-luna", Model.PricingPolicy [Model.InputPriceTier 272000 (Model.ModelCost 0.2 0.75 0.02 0.25) (Just 0.5)] (Just 0.25)),             (anthropicSpec, "claude-fable-5-1", Model.PricingPolicy [] (Just 20)),             (anthropicSpec, "claude-opus-5-5", Model.PricingPolicy [] (Just 8))           ]@@ -286,14 +311,16 @@                 "        \"thinkingStyle\": \"budget\",",                 "        \"supportsSamplingParameters\": true,",                 "        \"supportsFastMode\": false,",-                "        \"supportsForcedToolChoice\": true",+                "        \"supportsForcedToolChoice\": true,",+                "        \"supportsThinkingDisabled\": true",                 "      },"               ]               `Text.isInfixOf` rendered           ),-      testCase "an OpenAI model renders no compat block" $ do+      testCase "an unconstrained OpenAI model renders no compat block" $ do         upstream <- loadUpstream-        let rendered = renderText (catalogFor upstream openaiSpec)+        let catalog = catalogFor upstream openaiSpec+            rendered = renderText (catalog & #models .~ filter ((== "gpt-5.4") . (^. #modelId)) (catalog ^. #models))         assertBool           "no per-model compat block (the file-level \"compat\": \"auto\" stays)"           (not ("\"compat\": {" `Text.isInfixOf` rendered)),
test/GenModelsSpec.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE OverloadedRecordDot #-}+ module GenModelsSpec (tests) where  import Baikai.Api (Api (AnthropicMessages, OpenAIChatCompletions, OpenAIResponses))@@ -6,12 +8,15 @@     defaultAnthropicMessagesCompat,     supportsFastMode,     supportsSamplingParameters,+    supportsThinkingDisabled,     thinkingStyle,   )+import Baikai.Compat qualified as C import Baikai.Model (InputModality (InputText)) import Baikai.Model qualified import Control.Monad (forM_) import Data.Aeson qualified as Aeson+import Data.Aeson.KeyMap qualified as KM import Data.Text (Text) import Data.Text qualified as Text import GenModelsCore@@ -22,7 +27,31 @@ tests =   testGroup     "Baikai.GenModels"-    [ testCase "fast capability and rates must agree in both directions" $ do+    [ testCase "legacy OpenAI runtime JSON defaults missing native policy" $ do+        let legacy c = case Aeson.toJSON c of Aeson.Object o -> Aeson.Object (KM.delete "nativeCachePolicy" (KM.delete "cacheControlFormat" (KM.delete "supportedReasoningEfforts" o))); v -> v+        case Aeson.fromJSON (legacy C.defaultOpenAIResponsesCompat) of+          Aeson.Success c -> assertBool "absent Responses policy" ((c :: C.OpenAIResponsesCompat).nativeCachePolicy == Nothing)+          Aeson.Error err -> assertFailure err+        case Aeson.fromJSON (legacy C.defaultOpenAICompletionsCompat) of+          Aeson.Success c -> assertBool "absent Chat policy" ((c :: C.OpenAICompletionsCompat).nativeCachePolicy == Nothing)+          Aeson.Error err -> assertFailure err,+      testCase "native catalog rejects contradictory controls and unpriced Long naming model" $ do+        let conflicting = CatalogCompatOpenAI (C.defaultOpenAICompletionsCompat {C.cacheControlFormat = Just C.CacheControlFormatAnthropic, C.nativeCachePolicy = Just (C.NativeCacheModern False True)})+            unpriced = CatalogCompatResponses (C.defaultOpenAIResponsesCompat {C.nativeCachePolicy = Just (C.NativeCacheModern True True)})+            emptyLegacy = CatalogCompatResponses (C.defaultOpenAIResponsesCompat {C.nativeCachePolicy = Just (C.NativeCacheLegacy False False)})+        forM_ [conflicting, unpriced, emptyLegacy] $ \block -> do+          let c = collisionCatalog {models = [(model "invalid-cache-model") {entryCompatOverride = Just block}]}+          case checkNativeCacheCompat (flattenEntries c) of+            Left err -> assertBool "names model" ("invalid-cache-model" `Text.isInfixOf` err)+            Right () -> assertFailure "invalid policy accepted",+      testCase "legacy Anthropic model JSON does not invent disable support" $ do+        let raw = case Aeson.toJSON (defaultAnthropicMessagesCompat {supportsThinkingDisabled = True}) of+              Aeson.Object o -> Aeson.Object (KM.delete "supportsThinkingDisabled" o)+              other -> other+        case Aeson.fromJSON raw of+          Aeson.Error err -> assertFailure err+          Aeson.Success c -> assertBool "missing fact is conservative" (not (supportsThinkingDisabled c)),+      testCase "fast capability and rates must agree in both directions" $ do         forM_ [(True, Nothing), (False, Just (CostEntry 10 50 1 12.5))] $ \(supported, rates) -> do           let block = CatalogCompatAnthropic (defaultAnthropicMessagesCompat {supportsFastMode = supported})               catalog = (anthropicCatalog CatalogCompatAuto (Just block)) {models = [(model "claude-x") {entryCompatOverride = Just block, entryFastModeCost = rates}]}
test/StrictEvidenceSpec.hs view
@@ -195,6 +195,14 @@         "a thinking budget that will not fit the output ceiling is discarded"         (ThinkingDroppedBudgetExceeded ThinkingMax 32000 8192)         "does not fit inside the resolved output ceiling",+      refusesDowngrade+        "a host without controls may still think by default when none is requested"+        (ThinkingDroppedUnsupportedHost ThinkingNone)+        "exposes no reasoning controls",+      refusesDowngrade+        "a minimum positive effort cannot satisfy explicit off"+        (EffortClamped ThinkingNone "low")+        "would be sent as low",       testCase "several downgrades on one call are reported together" $         case checkEvidenceRequirements           (EvidenceRequired EvidenceRequestedOnly)
test/ThinkingLevelSpec.hs view
@@ -16,7 +16,8 @@  levels :: [(String, ThinkingLevel, Text, Integer)] levels =-  [ ("minimal", ThinkingMinimal, "minimal", 1024),+  [ ("none", ThinkingNone, "none", 0),+    ("minimal", ThinkingMinimal, "minimal", 1024),     ("low", ThinkingLow, "low", 2048),     ("medium", ThinkingMedium, "medium", 8192),     ("high", ThinkingHigh, "high", 16384),