diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/baikai-claude.cabal b/baikai-claude.cabal
--- a/baikai-claude.cabal
+++ b/baikai-claude.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.4
 name: baikai-claude
-version: 0.7.2.0
+version: 0.7.3.0
 synopsis: Anthropic Claude providers for the baikai abstraction
 description:
   Anthropic backends for baikai: the Messages API over SSE, the claude -p batch
@@ -72,7 +72,7 @@
   autogen-modules: Paths_baikai_claude
   build-depends:
     aeson ^>=2.2,
-    baikai ^>=0.9.0,
+    baikai ^>=0.10.0,
     base >=4.20 && <5,
     base16-bytestring ^>=1.0,
     base64-bytestring ^>=1.2,
@@ -126,7 +126,7 @@
 
   build-depends:
     aeson,
-    baikai ^>=0.9.0,
+    baikai ^>=0.10.0,
     baikai-claude,
     base >=4.20 && <5,
     bytestring,
diff --git a/src/Baikai/Provider/Claude/Agent.hs b/src/Baikai/Provider/Claude/Agent.hs
--- a/src/Baikai/Provider/Claude/Agent.hs
+++ b/src/Baikai/Provider/Claude/Agent.hs
@@ -176,15 +176,16 @@
   Just mid -> ["--model", Text.unpack mid]
 
 -- | Claude's @--effort@ accepts @low|medium|high|xhigh|max@, but not
--- @minimal@, so the lowest Baikai level maps up to @low@. This matches
--- "Baikai.Provider.Claude.Interactive" exactly so the two surfaces
--- cannot drift.
+-- @minimal@ or @none@, so the two lowest Baikai levels map up to @low@.
+-- This matches "Baikai.Provider.Claude.Interactive" exactly so the two
+-- surfaces cannot drift.
 effortArgs :: AgentRunRequest -> [String]
 effortArgs req = case req ^. #effort of
   Nothing -> []
   Just lvl -> ["--effort", Text.unpack (claudeEffortValue lvl)]
 
 claudeEffortValue :: ThinkingLevel -> Text
+claudeEffortValue ThinkingNone = "low"
 claudeEffortValue ThinkingMinimal = "low"
 claudeEffortValue lvl = renderThinkingLevel lvl
 
diff --git a/src/Baikai/Provider/Claude/Cli.hs b/src/Baikai/Provider/Claude/Cli.hs
--- a/src/Baikai/Provider/Claude/Cli.hs
+++ b/src/Baikai/Provider/Claude/Cli.hs
@@ -68,7 +68,7 @@
 import Baikai.ResponseFormat (ResponseFormat (..), declaredStructuredOutput)
 import Baikai.StopReason (StopReason (..))
 import Baikai.Stream (liftCompleteToStream)
-import Baikai.ThinkingLevel (ThinkingLevel (ThinkingMinimal), renderThinkingLevel)
+import Baikai.ThinkingLevel (ThinkingLevel (ThinkingMinimal, ThinkingNone), renderThinkingLevel)
 import Baikai.Usage (Usage, zeroUsage)
 import Control.Exception (SomeException, displayException, fromException)
 import Control.Exception qualified
@@ -173,7 +173,7 @@
     | otherwise -> Right (Internal.decodeUtf8Lenient (LBS.toStrict (Aeson.encode v)))
 
 -- | Render @--effort@ from 'Options.thinking'. Claude's @--effort@ has
--- no @minimal@, so the lowest Baikai level collapses to @low@; when
+-- no @minimal@ or @none@ effort value, so those levels map to @low@; when
 -- 'thinking' is unset, no effort flag is emitted.
 effortArgs :: Options -> [String]
 effortArgs opts = case opts ^. #thinking of
@@ -181,6 +181,7 @@
   Just lvl -> ["--effort", Text.unpack (claudeEffortValue lvl)]
 
 claudeEffortValue :: ThinkingLevel -> Text
+claudeEffortValue ThinkingNone = "low"
 claudeEffortValue ThinkingMinimal = "low"
 claudeEffortValue lvl = renderThinkingLevel lvl
 
diff --git a/src/Baikai/Provider/Claude/Interactive.hs b/src/Baikai/Provider/Claude/Interactive.hs
--- a/src/Baikai/Provider/Claude/Interactive.hs
+++ b/src/Baikai/Provider/Claude/Interactive.hs
@@ -103,13 +103,15 @@
   Just mid -> ["--model", Text.unpack mid]
 
 -- | Claude's @--effort@ accepts @low|medium|high|xhigh|max@, but
--- not @minimal@, so the lowest Baikai level maps up to @low@.
+-- not @minimal@ or @none@, so the two lowest Baikai levels map up to
+-- @low@.
 effortArgs :: InteractiveLaunchRequest -> [String]
 effortArgs req = case req ^. #effort of
   Nothing -> []
   Just lvl -> ["--effort", Text.unpack (claudeEffortValue lvl)]
 
 claudeEffortValue :: ThinkingLevel -> Text
+claudeEffortValue ThinkingNone = "low"
 claudeEffortValue ThinkingMinimal = "low"
 claudeEffortValue lvl = renderThinkingLevel lvl
 
diff --git a/src/Baikai/Provider/Claude/Internal/Request.hs b/src/Baikai/Provider/Claude/Internal/Request.hs
--- a/src/Baikai/Provider/Claude/Internal/Request.hs
+++ b/src/Baikai/Provider/Claude/Internal/Request.hs
@@ -347,6 +347,15 @@
 -- Display changes visibility only; it does not establish reasoning depth.
 -- Returning only the first is what this provider used to do, and it is
 -- why a caller could never tell an honoured request from a dropped one.
+--
+-- 'ThinkingNone' sends @thinking: {"type": "disabled"}@ when the
+-- generation accepts it ('supportsThinkingDisabled'), on either style:
+-- some adaptive generations otherwise think by default, and on a
+-- budget one the explicit value is documented as identical to omitting
+-- the field. It carries no effort and no display, because Anthropic
+-- rejects @display@ beside @disabled@. A generation that rejects
+-- @disabled@ falls through to the adaptive branch below, where 'adaptiveEffort'
+-- clamps @none@ to @low@ and the clamp is recorded.
 computeThinking ::
   AnthropicMessagesCompat ->
   Model ->
@@ -366,6 +375,41 @@
             adjustments = [ThinkingDroppedUnsupportedModel lvl]
           }
       )
+  | lvl == ThinkingNone && compat ^. #supportsThinkingDisabled =
+      -- Opus 5 and Haiku 5.5 accept @disabled@ only at effort @high@ or
+      -- below. That is safe here: this branch sends no effort, so
+      -- @output_config@ effort is omitted.
+      ( ThinkingPlan
+          { field = Just Messages.ThinkingDisabled,
+            effort = Nothing,
+            budget = Nothing
+          },
+        ThinkingTranslation
+          { requested = Just lvl,
+            mode = ThinkingModeToggle,
+            effortText = Nothing,
+            budgetTokens = Nothing,
+            wireField = Just "thinking",
+            displayText = Nothing,
+            adjustments = []
+          }
+      )
+  | lvl == ThinkingNone && thinkingStyle compat /= AnthropicThinkingAdaptive =
+      -- Legacy or hand-rolled compat records can reach this: a budget-style
+      -- host that rejects @disabled@. Omitting the field is the provider
+      -- default, which a budget-era generation documents as no thinking,
+      -- but nothing on the wire says so.
+      ( emptyThinkingPlan,
+        ThinkingTranslation
+          { requested = Just lvl,
+            mode = ThinkingModeUnsupported,
+            effortText = Nothing,
+            budgetTokens = Nothing,
+            wireField = Nothing,
+            displayText = Nothing,
+            adjustments = [EffortOmitted lvl]
+          }
+      )
   | thinkingStyle compat == AnthropicThinkingAdaptive =
       let e = adaptiveEffort lvl
        in ( ThinkingPlan
@@ -411,8 +455,13 @@
 -- <https://platform.claude.com/docs/en/build-with-claude/effort>), so an
 -- omitted field does not mean @high@. Sending a model's default
 -- explicitly is documented as identical to omitting it.
+--
+-- 'ThinkingNone' reaches this when disable support is absent from the
+-- compatibility record. The positive @low@ fallback is recorded, so it
+-- cannot pass a strict caller's explicit off requirement.
 adaptiveEffort :: ThinkingLevel -> Text
 adaptiveEffort = \case
+  ThinkingNone -> "low"
   ThinkingMinimal -> "low"
   ThinkingLow -> "low"
   ThinkingMedium -> "medium"
diff --git a/src/Baikai/Provider/Claude/Shape.hs b/src/Baikai/Provider/Claude/Shape.hs
--- a/src/Baikai/Provider/Claude/Shape.hs
+++ b/src/Baikai/Provider/Claude/Shape.hs
@@ -12,6 +12,7 @@
     replaceToolSchemas,
     injectToolChoiceNone,
     injectToolCacheControl,
+    injectSystemCacheControl,
   )
 where
 
@@ -36,7 +37,8 @@
   Aeson.Value ->
   Aeson.Value
 shapeRequestBody compat ctx opts =
-  injectToolCacheControl compat opts
+  injectSystemCacheControl compat opts
+    . injectToolCacheControl compat opts
     . injectToolChoiceNone opts
     . replaceToolSchemas ctx
 
@@ -72,6 +74,46 @@
       | Just marker <- cacheControlMarker compat retention ->
           mapObject (adjustKey (key "tools") (markLastTool marker)) body
     _ -> body
+
+-- | Put a @cache_control@ breakpoint on the system prompt when the
+-- caller asked for cache retention.
+--
+-- The top-level marker that
+-- 'Baikai.Provider.Claude.Internal.Request.computeCacheControl' sends is
+-- Anthropic's automatic caching: it places its breakpoint on the last
+-- cacheable block. Without a breakpoint on the shared prefix, changing
+-- the first user message prevents reuse of an earlier end-of-request
+-- entry. The system-block breakpoint caches that shared prefix
+-- (tools, then system) on its own.
+--
+-- A plain-string @system@ becomes a one-element array of text blocks
+-- carrying the same marker, with the same TTL as the top-level one, so
+-- Anthropic's "longer TTL first" ordering rule holds. Together with the
+-- automatic and the last-tool breakpoints this uses three of the four
+-- slots Anthropic allows. An absent or empty system prompt is left
+-- alone: Anthropic rejects empty text blocks.
+injectSystemCacheControl :: AnthropicMessagesCompat -> Options -> Aeson.Value -> Aeson.Value
+injectSystemCacheControl compat opts body =
+  case cacheRetention opts of
+    Just retention
+      | Just marker <- cacheControlMarker compat retention ->
+          mapObject (adjustKey (key "system") (markSystem marker)) body
+    _ -> body
+
+markSystem :: Aeson.Value -> Aeson.Value -> Aeson.Value
+markSystem marker = \case
+  String txt
+    | txt /= "" ->
+        Array
+          ( Vector.singleton
+              ( Aeson.object
+                  [ "type" .= ("text" :: Text),
+                    "text" .= txt,
+                    "cache_control" .= marker
+                  ]
+              )
+          )
+  other -> other
 
 mapTools :: [Tool.Tool] -> Aeson.Value -> Aeson.Value
 mapTools toolDefs = \case
diff --git a/test/EvidenceSpec.hs b/test/EvidenceSpec.hs
--- a/test/EvidenceSpec.hs
+++ b/test/EvidenceSpec.hs
@@ -50,7 +50,17 @@
     -- @--test-options='--pattern Evidence'@ actually selects it. A
     -- pattern that matches nothing reports "All 0 tests passed".
     "EvidenceSpec: Anthropic model-call evidence"
-    [ successEvidenceTest,
+    [ testCase "off remains requested and translated without inventing observed thinking" $ do
+        ev <- oneEvidence =<< replay 200 successHeaders successBody (baseOptions & #thinking .~ Just ThinkingNone)
+        field "observed_thinking" ev @?= Just (String "unobserved")
+        case field "thinking" ev of
+          Just (Object t) -> do
+            KeyMap.lookup "requested" t @?= Just (String "none")
+            KeyMap.lookup "mode" t @?= Just (String "toggle")
+            KeyMap.lookup "wire_field" t @?= Just (String "thinking")
+            KeyMap.lookup "adjustments" t @?= Just (Array Vector.empty)
+          other -> assertFailure ("missing thinking translation: " <> show other),
+      successEvidenceTest,
       rateLimitEvidenceTest,
       thinkingEvidenceTest,
       samplingEvidenceTest,
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -204,7 +204,8 @@
         claudeInteractiveCommand defaultClaudeInteractiveConfig req
           @?= Right ("claude", ["--effort", expected, "--", "prompt"])
     | (name, level, expected) <-
-        [ ("minimal as low", ThinkingMinimal, "low"),
+        [ ("none as low", ThinkingNone, "low"),
+          ("minimal as low", ThinkingMinimal, "low"),
           ("low", ThinkingLow, "low"),
           ("medium", ThinkingMedium, "medium"),
           ("high", ThinkingHigh, "high"),
@@ -374,7 +375,8 @@
                 "plan"
               ]
     | (name, level, expected) <-
-        [ ("minimal as low", ThinkingMinimal, "low"),
+        [ ("none as low", ThinkingNone, "low"),
+          ("minimal as low", ThinkingMinimal, "low"),
           ("low", ThinkingLow, "low"),
           ("medium", ThinkingMedium, "medium"),
           ("high", ThinkingHigh, "high"),
@@ -473,6 +475,7 @@
               [ -- claude's --effort has no minimal, so the lowest level
                 -- collapses and a run at minimal is wire-identical to a
                 -- run at low.
+                (ThinkingNone, "low", [EffortClamped ThinkingNone "low"]),
                 (ThinkingMinimal, "low", [EffortClamped ThinkingMinimal "low"]),
                 (ThinkingLow, "low", []),
                 (ThinkingMedium, "medium", []),
@@ -680,7 +683,8 @@
                 ]
               )
     | (name, level, expected) <-
-        [ ("minimal as low", ThinkingMinimal, "low"),
+        [ ("none as low", ThinkingNone, "low"),
+          ("minimal as low", ThinkingMinimal, "low"),
           ("max", ThinkingMax, "max")
         ]
     ]
diff --git a/test/ShapeSpec.hs b/test/ShapeSpec.hs
--- a/test/ShapeSpec.hs
+++ b/test/ShapeSpec.hs
@@ -3,7 +3,7 @@
 import Baikai
 import Baikai.Models.Generated qualified as Models
 import Baikai.Provider.Claude.Internal.Request (describeThinkingFor, mapRequest)
-import Baikai.Provider.Claude.Shape (streamRequestBody)
+import Baikai.Provider.Claude.Shape (injectSystemCacheControl, streamRequestBody)
 import Baikai.Provider.Claude.Transport qualified as Transport
 import Control.Lens ((&), (.~), (^.))
 import Control.Monad (forM_)
@@ -29,7 +29,8 @@
       verbatimToolSchemaTest,
       toolChoiceNoneTest,
       toolCacheControlTest,
-      toolCacheControlCompatGateTest
+      toolCacheControlCompatGateTest,
+      systemCacheControlTests
     ]
 
 verbatimToolSchemaTest :: TestTree
@@ -107,6 +108,100 @@
         opts = emptyOptions & #cacheRetention .~ Just CacheRetentionShort
     value <- shapedBody model ctx opts
     lookupPath ["tools", "0", "cache_control"] value @?= Nothing
+
+systemCacheControlTests :: TestTree
+systemCacheControlTests =
+  testGroup
+    "system prompt cache breakpoint"
+    [ testCase "without retention the system prompt stays a plain string" $ do
+        value <- shapedBody Models.anthropic_claude_haiku_4_5 systemCtx emptyOptions
+        lookupPath ["system"] value @?= Just (String systemText)
+        lookupPath ["cache_control"] value @?= Nothing,
+      testCase "CacheRetentionNone keeps the system prompt a plain string" $ do
+        value <- shapedBody Models.anthropic_claude_haiku_4_5 systemCtx (retention CacheRetentionNone)
+        lookupPath ["system"] value @?= Just (String systemText)
+        lookupPath ["cache_control"] value @?= Nothing,
+      testCase "short retention sends the system prompt as one ephemeral block" $ do
+        value <- shapedBody Models.anthropic_claude_haiku_4_5 systemCtx (retention CacheRetentionShort)
+        lookupPath ["system"] value @?= Just (systemBlocks ephemeral)
+        lookupPath ["cache_control"] value @?= Just ephemeral,
+      testCase "long retention gives the system block the 1h ttl" $ do
+        value <- shapedBody Models.anthropic_claude_haiku_4_5 systemCtx (retention CacheRetentionLong)
+        lookupPath ["system"] value @?= Just (systemBlocks oneHour)
+        lookupPath ["cache_control"] value @?= Just oneHour,
+      testCase "long retention downgrades with the host" $ do
+        let model =
+              Models.anthropic_claude_haiku_4_5
+                & #compat .~ CompatAnthropicMessages (defaultAnthropicMessagesCompat & #supportsLongCacheRetention .~ False)
+        value <- shapedBody model systemCtx (retention CacheRetentionLong)
+        lookupPath ["system"] value @?= Just (systemBlocks ephemeral)
+        lookupPath ["cache_control"] value @?= Just ephemeral,
+      testCase "without a system prompt no system field appears" $ do
+        value <- shapedBody Models.anthropic_claude_haiku_4_5 emptyContext (retention CacheRetentionShort)
+        lookupPath ["system"] value @?= Nothing
+        lookupPath ["cache_control"] value @?= Just ephemeral,
+      testCase "tools, system and automatic markers stay within four breakpoints" $ do
+        let ctx =
+              systemCtx
+                & #tools
+                  .~ Vector.singleton
+                    (emptyTool & #name .~ "lookup" & #description .~ "Lookup" & #parameters .~ objectSchema)
+        value <- shapedBody Models.anthropic_claude_haiku_4_5 ctx (retention CacheRetentionShort)
+        lookupPath ["tools", "0", "cache_control"] value @?= Just ephemeral
+        lookupPath ["system", "0", "cache_control"] value @?= Just ephemeral
+        lookupPath ["cache_control"] value @?= Just ephemeral,
+      testCase "empty system text never becomes an empty cacheable block" $
+        forM_ [CacheRetentionShort, CacheRetentionLong] $ \r -> do
+          value <- shapedBody Models.anthropic_claude_haiku_4_5 (emptyContext & #systemPrompt .~ Just "") (retention r)
+          lookupPath ["system"] value @?= Just (String ""),
+      testCase "system caching is independent of tool-marker support" $ do
+        let model =
+              Models.anthropic_claude_haiku_4_5
+                & #compat .~ CompatAnthropicMessages (defaultAnthropicMessagesCompat & #supportsCacheControlOnTools .~ False)
+            ctx = systemCtx & #tools .~ Vector.singleton (emptyTool & #name .~ "lookup" & #parameters .~ objectSchema)
+        value <- shapedBody model ctx (retention CacheRetentionLong)
+        lookupPath ["tools", "0", "cache_control"] value @?= Nothing
+        lookupPath ["system"] value @?= Just (systemBlocks oneHour)
+        lookupPath ["cache_control"] value @?= Just oneHour,
+      testCase "all markers share the effective TTL with multiple tools" $
+        forM_ [True, False] $ \supportsLong -> do
+          let model =
+                Models.anthropic_claude_haiku_4_5
+                  & #compat .~ CompatAnthropicMessages (defaultAnthropicMessagesCompat & #supportsLongCacheRetention .~ supportsLong)
+              ctx = systemCtx & #tools .~ Vector.fromList [emptyTool & #name .~ "first", emptyTool & #name .~ "last"]
+              marker = if supportsLong then oneHour else ephemeral
+          value <- shapedBody model ctx (retention CacheRetentionLong)
+          lookupPath ["tools", "0", "cache_control"] value @?= Nothing
+          lookupPath ["tools", "1", "cache_control"] value @?= Just marker
+          lookupPath ["system"] value @?= Just (systemBlocks marker)
+          lookupPath ["cache_control"] value @?= Just marker,
+      testCase "different first questions retain the same cacheable system prefix" $ do
+        let ctxFor question = systemCtx & #messages .~ Vector.singleton (user question)
+        first <- shapedBody Models.anthropic_claude_haiku_4_5 (ctxFor "First question") (retention CacheRetentionShort)
+        second <- shapedBody Models.anthropic_claude_haiku_4_5 (ctxFor "Different question") (retention CacheRetentionShort)
+        lookupPath ["system"] first @?= lookupPath ["system"] second
+        assertBool "user messages remain distinct" (lookupPath ["messages"] first /= lookupPath ["messages"] second),
+      testCase "system shaping preserves other fields and existing block arrays" $ do
+        let body = Aeson.object ["system" .= systemText, "unrelated" .= objectSchema]
+            expected = Aeson.object ["system" .= systemBlocks ephemeral, "unrelated" .= objectSchema]
+            inject = injectSystemCacheControl defaultAnthropicMessagesCompat (retention CacheRetentionShort)
+        inject body @?= expected
+        inject expected @?= expected
+    ]
+  where
+    systemText = "You are a careful assistant." :: Text.Text
+    systemCtx = emptyContext & #systemPrompt .~ Just systemText
+    retention r = emptyOptions & #cacheRetention .~ Just r
+    ephemeral = Aeson.object ["type" .= ("ephemeral" :: Text.Text)]
+    oneHour = Aeson.object ["type" .= ("ephemeral" :: Text.Text), "ttl" .= ("1h" :: Text.Text)]
+    systemBlocks marker =
+      Aeson.toJSON
+        [ Aeson.object
+            [ "type" .= ("text" :: Text.Text),
+              "text" .= systemText,
+              "cache_control" .= marker
+            ]
+        ]
 
 shapedBody :: Model -> Context -> Options -> IO Value
 shapedBody model ctx opts = do
diff --git a/test/ThinkingSpec.hs b/test/ThinkingSpec.hs
--- a/test/ThinkingSpec.hs
+++ b/test/ThinkingSpec.hs
@@ -14,6 +14,7 @@
 import Claude.V1.Messages qualified as Messages
 import Control.Lens ((&), (.~), (^.))
 import Data.Aeson qualified as Aeson
+import Data.Aeson.KeyMap qualified as KeyMap
 import Data.ByteString.Lazy qualified as BSL
 import Data.Char qualified as Char
 import Data.Generics.Labels ()
@@ -23,6 +24,7 @@
 import Data.Time.Clock (UTCTime)
 import Data.Vector qualified as Vector
 import Numeric.Natural (Natural)
+import Streamly.Data.Stream qualified as Stream
 import Test.Tasty (TestTree, testGroup)
 import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))
 
@@ -48,6 +50,7 @@
       tooSmallCapDropsThinkingTest,
       mergedOutputConfigTest,
       explicitCompatOverridesDefaultTest,
+      thinkingNoneTests,
       anthropicModelsCoverCatalogTest,
       testGroup "fast-mode catalog facts" [testCase name ((anthropicMessagesCompatFor m ^. #supportsFastMode) @?= fast) | (name, m, _, _, _, fast) <- anthropicModels],
       testGroup "forced-tool catalog facts" [testCase name ((anthropicMessagesCompatFor m ^. #supportsForcedToolChoice) @?= forced) | (name, m, _, _, forced, _) <- anthropicModels],
@@ -355,6 +358,126 @@
     requestThinking req @?= Just (Messages.ThinkingAdaptiveWithDisplay Messages.ThinkingSummarized)
     (req ^. #output_config >>= Messages.effort) @?= Just "low"
 
+-- | 'ThinkingNone' against every catalog model, and the wire bytes it
+-- and the settings it sits beside produce.
+--
+-- Whether a model accepts @thinking: {"type": "disabled"}@ is pinned
+-- by hand in @CatalogSpec@; here it is read off the record, because
+-- what is under test is the request built from it.
+thinkingNoneTests :: TestTree
+thinkingNoneTests =
+  testGroup
+    "ThinkingNone"
+    ( [ testCase (name <> " none") $ do
+          let opts = emptyOptions & #thinking .~ Just ThinkingNone
+          (req, t) <- mappedFor model emptyContext opts
+          t ^. #requested @?= Just ThinkingNone
+          if anthropicMessagesCompatFor model ^. #supportsThinkingDisabled
+            then do
+              requestThinking req @?= Just Messages.ThinkingDisabled
+              (req ^. #output_config >>= Messages.effort) @?= Nothing
+              req ^. #max_tokens @?= model ^. #maxOutputTokens
+              t ^. #mode @?= ThinkingModeToggle
+              t ^. #wireField @?= Just "thinking"
+              t ^. #effortText @?= Nothing
+              t ^. #displayText @?= Nothing
+              t ^. #adjustments @?= []
+            else do
+              -- Anthropic rejects "disabled" here, so the least thinking
+              -- the model can be asked for is adaptive at low effort.
+              requestThinking req @?= Just (Messages.ThinkingAdaptiveWithDisplay Messages.ThinkingSummarized)
+              (req ^. #output_config >>= Messages.effort) @?= Just "low"
+              t ^. #mode @?= ThinkingModeAdaptive
+              t ^. #adjustments @?= [EffortClamped ThinkingNone "low"]
+      | (name, model, _, _, _, _) <- anthropicModels
+      ]
+        <> [ testCase "Haiku 5.5 none sends exactly {\"type\":\"disabled\"} and no output_config" $ do
+               body <- requestBody anthropic_claude_haiku_5_5 (emptyOptions & #thinking .~ Just ThinkingNone)
+               KeyMap.lookup "thinking" body @?= Just (Aeson.object ["type" Aeson..= ("disabled" :: Text.Text)])
+               KeyMap.member "output_config" body @?= False,
+             testCase "Haiku 5.5 with no level still omits thinking and output_config" $ do
+               -- Nothing leaves the model's own default in force; on an
+               -- adaptive generation that is to think. Unchanged by the
+               -- arrival of ThinkingNone.
+               body <- requestBody anthropic_claude_haiku_5_5 emptyOptions
+               KeyMap.member "thinking" body @?= False
+               KeyMap.member "output_config" body @?= False,
+             testCase "Haiku 5.5 low still sends adaptive with summarized display" $ do
+               body <- requestBody anthropic_claude_haiku_5_5 (emptyOptions & #thinking .~ Just ThinkingLow)
+               KeyMap.lookup "thinking" body
+                 @?= Just
+                   ( Aeson.object
+                       [ "type" Aeson..= ("adaptive" :: Text.Text),
+                         "display" Aeson..= ("summarized" :: Text.Text)
+                       ]
+                   )
+               KeyMap.lookup "output_config" body
+                 @?= Just (Aeson.object ["effort" Aeson..= ("low" :: Text.Text)]),
+             testCase "none keeps a JSON-schema output_config without an effort" $ do
+               let schema = Aeson.object ["type" Aeson..= ("object" :: Text.Text)]
+                   opts =
+                     emptyOptions
+                       & #thinking .~ Just ThinkingNone
+                       & #responseFormat .~ Just (JsonSchema (jsonSchemaFormat "answer" schema))
+               req <- requestFor anthropic_claude_haiku_5_5 opts
+               requestThinking req @?= Just Messages.ThinkingDisabled
+               req ^. #output_config @?= Just (Messages.jsonSchemaConfig schema),
+             testCase "a budget-style host that rejects disabled omits the field and says so" $ do
+               let model =
+                     anthropic_claude_haiku_4_5
+                       & #compat .~ CompatAnthropicMessages (defaultAnthropicMessagesCompat {supportsThinkingDisabled = False})
+                   opts = emptyOptions & #thinking .~ Just ThinkingNone
+               (req, t) <- mappedFor model emptyContext opts
+               requestThinking req @?= Nothing
+               req ^. #max_tokens @?= model ^. #maxOutputTokens
+               t ^. #mode @?= ThinkingModeUnsupported
+               t ^. #wireField @?= Nothing
+               t ^. #adjustments @?= [EffortOmitted ThinkingNone],
+             testCase "strict complete and stream refuse an always-on model before dispatch" $ do
+               let opts = emptyOptions & #thinking .~ Just ThinkingNone & #evidence .~ Just (evidenceRequest "off" & #strictness .~ EvidenceRequired EvidenceRequestedOnly)
+                   provider = apiProviderWith AnthropicMessages (\_ _ _ -> error "stream dispatched") (\_ _ _ -> error "complete dispatched") & #describeThinking .~ describeThinkingFor & #strengthCeiling .~ EvidenceModelObserved
+               reg <- newProviderRegistry
+               registerApiProviderWith reg provider
+               response <- completeRequestWith reg anthropic_claude_opus_5_5 emptyContext opts
+               case responseError response of
+                 Just err -> err ^. #category @?= InvalidRequest
+                 Nothing -> assertFailure "strict off unexpectedly succeeded"
+               events <- Stream.toList (streamRequestWith reg anthropic_claude_opus_5_5 emptyContext opts)
+               case events of
+                 [EventStart _, EventError payload] -> (payload ^. #errorInfo) /= Nothing @?= True
+                 other -> assertFailure ("expected pre-dispatch stream refusal: " <> show other),
+             testCase "strict evidence accepts none where disabled is sent" $
+               checkEvidenceRequirements
+                 (EvidenceRequired EvidenceRequestedOnly)
+                 (declaredStrength AnthropicMessages)
+                 (describeThinkingFor anthropic_claude_haiku_5_5 (emptyOptions & #thinking .~ Just ThinkingNone))
+                 @?= [],
+             testCase "strict evidence refuses none where it is clamped to low" $
+               assertBool "Opus 5.5 cannot turn thinking off, so a strict caller is refused" $
+                 not . null $
+                   checkEvidenceRequirements
+                     (EvidenceRequired EvidenceRequestedOnly)
+                     (declaredStrength AnthropicMessages)
+                     (describeThinkingFor anthropic_claude_opus_5_5 (emptyOptions & #thinking .~ Just ThinkingNone)),
+             testCase "strict evidence accepts none on a model that cannot reason" $ do
+               let model = anthropic_claude_haiku_4_5 & #reasoning .~ False
+                   opts = emptyOptions & #thinking .~ Just ThinkingNone
+               t <- translationFor model opts
+               t ^. #adjustments @?= [ThinkingDroppedUnsupportedModel ThinkingNone]
+               checkEvidenceRequirements
+                 (EvidenceRequired EvidenceRequestedOnly)
+                 (declaredStrength AnthropicMessages)
+                 t
+                 @?= []
+           ]
+    )
+  where
+    requestBody model opts = do
+      req <- requestFor model opts
+      case Aeson.toJSON req of
+        Aeson.Object o -> pure o
+        other -> assertFailure ("request did not encode as an object: " <> show other)
+
 requestFor :: Model -> Options -> IO Messages.CreateMessage
 requestFor model opts = fst <$> mappedFor model emptyContext opts
 
@@ -372,6 +495,7 @@
 
 adaptiveEffort :: ThinkingLevel -> Maybe Text.Text
 adaptiveEffort = \case
+  ThinkingNone -> Just "low"
   ThinkingMinimal -> Just "low"
   ThinkingLow -> Just "low"
   ThinkingMedium -> Just "medium"
