claude 1.1.0 → 1.2.0
raw patch · 8 files changed
+181/−2 lines, 8 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Claude.V1.Messages: ContentBlock_Redacted_Thinking :: Text -> ContentBlock
+ Claude.V1.Messages: ContentBlock_Thinking :: Text -> Text -> ContentBlock
+ Claude.V1.Messages: Content_Redacted_Thinking :: Text -> Content
+ Claude.V1.Messages: Content_Thinking :: Text -> Text -> Content
+ Claude.V1.Messages: Delta_Signature_Delta :: Text -> ContentBlockDelta
+ Claude.V1.Messages: Delta_Thinking_Delta :: Text -> ContentBlockDelta
+ Claude.V1.Messages: Model_Context_Window_Exceeded :: StopReason
+ Claude.V1.Messages: ThinkingAdaptive :: Thinking
+ Claude.V1.Messages: ThinkingEnabled :: Natural -> Thinking
+ Claude.V1.Messages: [budget_tokens] :: Thinking -> Natural
+ Claude.V1.Messages: [signature] :: ContentBlockDelta -> Text
+ Claude.V1.Messages: [thinking] :: ContentBlockDelta -> Text
+ Claude.V1.Messages: data Thinking
+ Claude.V1.Messages: instance Data.Aeson.Types.FromJSON.FromJSON Claude.V1.Messages.Thinking
+ Claude.V1.Messages: instance Data.Aeson.Types.ToJSON.ToJSON Claude.V1.Messages.Thinking
+ Claude.V1.Messages: instance GHC.Classes.Eq Claude.V1.Messages.Thinking
+ Claude.V1.Messages: instance GHC.Generics.Generic Claude.V1.Messages.Thinking
+ Claude.V1.Messages: instance GHC.Show.Show Claude.V1.Messages.Thinking
- Claude.V1.Messages: CreateMessage :: Text -> Vector Message -> Natural -> Maybe Text -> Maybe Double -> Maybe Double -> Maybe Natural -> Maybe (Vector Text) -> Maybe Bool -> Maybe (Map Text Text) -> Maybe (Vector ToolDefinition) -> Maybe ToolChoice -> Maybe Text -> Maybe OutputConfig -> CreateMessage
+ Claude.V1.Messages: CreateMessage :: Text -> Vector Message -> Natural -> Maybe Text -> Maybe Double -> Maybe Double -> Maybe Natural -> Maybe (Vector Text) -> Maybe Bool -> Maybe (Map Text Text) -> Maybe (Vector ToolDefinition) -> Maybe ToolChoice -> Maybe Text -> Maybe OutputConfig -> Maybe Thinking -> CreateMessage
Files
- CHANGELOG.md +9/−0
- claude.cabal +1/−1
- examples/claude-programmatic-tool-calling-example/Main.hs +4/−0
- examples/claude-stream-example/Main.hs +2/−0
- examples/claude-tool-example/Main.hs +4/−0
- examples/claude-tool-search-example/Main.hs +4/−0
- src/Claude/V1/Messages.hs +72/−1
- tasty/Main.hs +85/−0
CHANGELOG.md view
@@ -1,3 +1,12 @@+1.2.0:++- Add `Thinking` type with `ThinkingAdaptive` and `ThinkingEnabled` constructors+ - `thinking` field on `CreateMessage`+ - `ContentBlock_Thinking` and `ContentBlock_Redacted_Thinking` response blocks+ - `Content_Thinking` and `Content_Redacted_Thinking` for replaying thinking in multi-turn+ - `Delta_Thinking_Delta` and `Delta_Signature_Delta` for streaming+- Add `Model_Context_Window_Exceeded` constructor to `StopReason`+ 1.1.0: - Replace `output_format` with `output_config` on `CreateMessage` (breaking change)
claude.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.4 name: claude-version: 1.1.0+version: 1.2.0 synopsis: Servant bindings to Anthropic's Claude API description: This package provides comprehensive and type-safe bindings to Anthropic's Claude API, providing both a Servant interface
examples/claude-programmatic-tool-calling-example/Main.hs view
@@ -249,6 +249,10 @@ where unless False action = action unless True _ = pure ()+printContentBlock (Messages.ContentBlock_Thinking{}) =+ Text.IO.putStrLn " [Thinking]"+printContentBlock (Messages.ContentBlock_Redacted_Thinking{}) =+ Text.IO.putStrLn " [Redacted thinking]" printContentBlock (Messages.ContentBlock_Unknown{ Messages.type_ = t }) = Text.IO.putStrLn $ " [Unknown] " <> t
examples/claude-stream-example/Main.hs view
@@ -27,6 +27,8 @@ case d of Messages.Delta_Text_Delta{ Messages.text = t } -> TIO.putStr t >> hFlush stdout+ Messages.Delta_Thinking_Delta{ Messages.thinking = t } ->+ TIO.putStr t >> hFlush stdout _ -> pure () -- Print newline when message is done Messages.Message_Stop -> putStrLn ""
examples/claude-tool-example/Main.hs view
@@ -186,5 +186,9 @@ Text.IO.putStrLn $ "[Tool search result for: " <> tid <> "]" printContent (Messages.ContentBlock_Code_Execution_Tool_Result{ Messages.tool_use_id = tid, Messages.code_execution_content = _ }) = Text.IO.putStrLn $ "[Code execution result for: " <> tid <> "]"+printContent (Messages.ContentBlock_Thinking{}) =+ Text.IO.putStrLn "[Thinking]"+printContent (Messages.ContentBlock_Redacted_Thinking{}) =+ Text.IO.putStrLn "[Redacted thinking]" printContent (Messages.ContentBlock_Unknown{ Messages.type_ = t }) = Text.IO.putStrLn $ "[Unknown block type: " <> t <> "]"
examples/claude-tool-search-example/Main.hs view
@@ -193,5 +193,9 @@ Text.IO.putStrLn " Unknown result type" printContent (Messages.ContentBlock_Code_Execution_Tool_Result{ Messages.tool_use_id = tid }) = Text.IO.putStrLn $ " [code_execution_result] for tool_use_id: " <> tid+printContent (Messages.ContentBlock_Thinking{}) =+ Text.IO.putStrLn " [thinking]"+printContent (Messages.ContentBlock_Redacted_Thinking{}) =+ Text.IO.putStrLn " [redacted_thinking]" printContent (Messages.ContentBlock_Unknown{ Messages.type_ = t }) = Text.IO.putStrLn $ " [unknown] type: " <> t
src/Claude/V1/Messages.hs view
@@ -13,6 +13,8 @@ , jsonSchemaConfig , jsonSchemaFormat , effortConfig+ -- * Thinking+ , Thinking(..) -- * Content types , Content(..) , ContentBlock(..)@@ -194,6 +196,8 @@ , input :: Value } | Content_Tool_Result { tool_use_id :: Text, content :: Maybe Text, is_error :: Maybe Bool }+ | Content_Thinking { thinking :: Text, signature :: Text }+ | Content_Redacted_Thinking { data_ :: Text } deriving stock (Generic, Show) -- | Create a text content block without cache control@@ -245,6 +249,10 @@ } contentBlockToContent ContentBlock_Tool_Search_Tool_Result{} = Nothing contentBlockToContent ContentBlock_Code_Execution_Tool_Result{} = Nothing+contentBlockToContent (ContentBlock_Thinking t sig) =+ Just Content_Thinking{ thinking = t, signature = sig }+contentBlockToContent (ContentBlock_Redacted_Thinking d) =+ Just Content_Redacted_Thinking{ data_ = d } contentBlockToContent ContentBlock_Unknown{} = Nothing -- | A reference to a tool found by tool search@@ -411,6 +419,8 @@ { tool_use_id :: Text , code_execution_content :: CodeExecutionToolResultContent }+ | ContentBlock_Thinking { thinking :: Text, signature :: Text }+ | ContentBlock_Redacted_Thinking { data_ :: Text } | ContentBlock_Unknown { type_ :: Text, raw :: Value } deriving stock (Generic, Show) @@ -442,6 +452,11 @@ { tool_use_id = toolUseId , code_execution_content = execContent }+ "thinking" -> ContentBlock_Thinking+ <$> o Aeson..: "thinking"+ <*> o Aeson..: "signature"+ "redacted_thinking" -> ContentBlock_Redacted_Thinking+ <$> o Aeson..: "data" _ -> pure (ContentBlock_Unknown t (Aeson.Object o)) instance ToJSON ContentBlock where@@ -471,6 +486,15 @@ , "tool_use_id" Aeson..= toolUseId , "content" Aeson..= execContent ]+ toJSON (ContentBlock_Thinking t sig) = Aeson.object+ [ "type" Aeson..= ("thinking" :: Text)+ , "thinking" Aeson..= t+ , "signature" Aeson..= sig+ ]+ toJSON (ContentBlock_Redacted_Thinking d) = Aeson.object+ [ "type" Aeson..= ("redacted_thinking" :: Text)+ , "data" Aeson..= d+ ] toJSON (ContentBlock_Unknown _typeName rawVal) = rawVal -- | A message in the conversation@@ -496,7 +520,9 @@ | Stop_Sequence | Tool_Use | Refusal- -- ^ Model refused the request for safety reasons (structured outputs only)+ -- ^ Model refused the request for safety reasons+ | Model_Context_Window_Exceeded+ -- ^ Generation stopped because the context window limit was reached (Claude 4.5+) deriving stock (Eq, Generic, Show) stopReasonOptions :: Options@@ -664,6 +690,43 @@ , format = Just $ jsonSchemaFormat s } +-- | Thinking configuration for extended thinking+--+-- * 'ThinkingAdaptive': Let Claude decide when and how much to think (Opus 4.6+).+-- Use the effort parameter ('effortConfig') to guide thinking depth.+-- * 'ThinkingEnabled': Enable thinking with a fixed token budget+-- (all models; deprecated on Opus 4.6).+--+-- @+-- -- Adaptive thinking (recommended for Opus 4.6)+-- thinking = Just ThinkingAdaptive+--+-- -- Manual thinking with budget (older models)+-- thinking = Just ThinkingEnabled{ budget_tokens = 32000 }+-- @+data Thinking+ = ThinkingAdaptive+ | ThinkingEnabled { budget_tokens :: Natural }+ deriving stock (Eq, Generic, Show)++instance FromJSON Thinking where+ parseJSON = Aeson.withObject "Thinking" $ \o -> do+ t <- o Aeson..: "type"+ case (t :: Text) of+ "adaptive" -> pure ThinkingAdaptive+ "enabled" -> do+ budget <- o Aeson..: "budget_tokens"+ pure ThinkingEnabled{ budget_tokens = budget }+ _ -> fail $ "Unknown thinking type: " <> show t++instance ToJSON Thinking where+ toJSON ThinkingAdaptive = Aeson.object+ [ "type" Aeson..= ("adaptive" :: Text) ]+ toJSON (ThinkingEnabled budget) = Aeson.object+ [ "type" Aeson..= ("enabled" :: Text)+ , "budget_tokens" Aeson..= budget+ ]+ -- | Create a JSON schema output format jsonSchemaFormat :: Value -> OutputFormat jsonSchemaFormat s = OutputFormat@@ -692,6 +755,9 @@ , output_config :: Maybe OutputConfig -- ^ Output configuration: effort level and/or structured output format. -- Use 'effortConfig', 'jsonSchemaConfig', or construct 'OutputConfig' directly.+ , thinking :: Maybe Thinking+ -- ^ Thinking configuration. Use 'ThinkingAdaptive' for Opus 4.6+ -- or 'ThinkingEnabled' with @budget_tokens@ for older models. } deriving stock (Generic, Show) instance FromJSON CreateMessage where@@ -717,6 +783,7 @@ , tool_choice = Nothing , container = Nothing , output_config = Nothing+ , thinking = Nothing } -- | Text delta in streaming@@ -740,6 +807,8 @@ data ContentBlockDelta = Delta_Text_Delta { text :: Text } | Delta_Input_Json_Delta { partial_json :: Text }+ | Delta_Thinking_Delta { thinking :: Text }+ | Delta_Signature_Delta { signature :: Text } deriving stock (Generic, Show) contentBlockDeltaOptions :: Options@@ -749,6 +818,8 @@ , constructorTagModifier = \s -> case s of "Delta_Text_Delta" -> "text_delta" "Delta_Input_Json_Delta" -> "input_json_delta"+ "Delta_Thinking_Delta" -> "thinking_delta"+ "Delta_Signature_Delta" -> "signature_delta" _ -> s }
tasty/Main.hs view
@@ -339,6 +339,84 @@ HUnit.assertFailure "user_id should be present and be an integer" _ -> HUnit.assertFailure "Expected exactly one tool_use block" + let adaptiveThinkingTest =+ HUnit.testCase "Create message - adaptive thinking" do+ Messages.MessageResponse{ content } <-+ createMessage+ Messages._CreateMessage+ { Messages.model = "claude-opus-4-6"+ , Messages.messages =+ [ Messages.Message+ { Messages.role = Messages.User+ , Messages.content =+ [ Messages.textContent "What is 2+2? Answer with just the number."+ ]+ , Messages.cache_control = Nothing+ }+ ]+ , Messages.max_tokens = 4096+ , Messages.thinking = Just Messages.ThinkingAdaptive+ }++ -- Should have at least one text block+ let hasText = any isTextBlock (toList content)+ HUnit.assertBool "Should have text content" hasText++ -- Thinking blocks may or may not be present (model decides),+ -- but verify we can parse them without errors+ let thinkingBlocks =+ [ ()+ | Messages.ContentBlock_Thinking{} <- toList content+ ]+ let redactedBlocks =+ [ ()+ | Messages.ContentBlock_Redacted_Thinking{} <- toList content+ ]+ -- Just verify counts are non-negative (always true, but exercises the pattern matches)+ HUnit.assertBool "Thinking block count should be non-negative"+ (length thinkingBlocks >= 0 && length redactedBlocks >= 0)++ let adaptiveThinkingStreamingTest =+ HUnit.testCase "Create message - adaptive thinking streaming" do+ textAcc <- IORef.newIORef Text.empty+ thinkingAcc <- IORef.newIORef Text.empty+ done <- Concurrent.newEmptyMVar++ let onEvent (Left _err) = Concurrent.putMVar done ()+ onEvent (Right ev) = case ev of+ Messages.Content_Block_Delta{ Messages.delta = d } ->+ case d of+ Messages.Delta_Text_Delta{ Messages.text = t } ->+ IORef.modifyIORef' textAcc (<> t)+ Messages.Delta_Thinking_Delta{ Messages.thinking = t } ->+ IORef.modifyIORef' thinkingAcc (<> t)+ _ -> pure ()+ Messages.Message_Stop ->+ Concurrent.putMVar done ()+ _ -> pure ()++ createMessageStreamTyped+ Messages._CreateMessage+ { Messages.model = "claude-opus-4-6"+ , Messages.messages =+ [ Messages.Message+ { Messages.role = Messages.User+ , Messages.content =+ [ Messages.textContent "What is 2+2? Answer with just the number."+ ]+ , Messages.cache_control = Nothing+ }+ ]+ , Messages.max_tokens = 4096+ , Messages.thinking = Just Messages.ThinkingAdaptive+ }+ onEvent++ _ <- Concurrent.takeMVar done+ text <- IORef.readIORef textAcc+ HUnit.assertBool "Expected non-empty streamed text"+ (not (Text.null text))+ -- Tool search test requires beta header let betaOptions = V1.defaultClientOptions { V1.apiKey = Text.pack key@@ -557,11 +635,18 @@ , tokenCountingTest , jsonOutputsTest , strictToolUseTest+ , adaptiveThinkingTest+ , adaptiveThinkingStreamingTest , toolSearchTest , programmaticToolCallingTest ] Tasty.defaultMain (Tasty.testGroup "Claude API Tests" tests)++-- | Check if a content block is a text block+isTextBlock :: Messages.ContentBlock -> Bool+isTextBlock (Messages.ContentBlock_Text{}) = True+isTextBlock _ = False -- | Check if a content block is a server_tool_use for code_execution isCodeExecutionServerToolUse :: Messages.ContentBlock -> Bool