Liking cljdoc? Tell your friends :D

anthropic-clj

Clojars Project cljdoc test

An idiomatic Clojure wrapper over the official Anthropic Java SDK (com.anthropic/anthropic-java). Build a request as a Clojure map, get a Clojure map back.

Unofficial. A community library, not affiliated with or endorsed by Anthropic. It wraps Anthropic's official Java SDK; it is not itself an official Anthropic SDK.

Stack

Clojure Anthropic jsonista

Why

Every other Clojure Anthropic library hand-rolls HTTP against the REST API, which means each one is perpetually chasing Anthropic's surface and tends to fall behind. This one wraps Anthropic's own actively-maintained Java SDK instead, so streaming, tool use, retries, and new model ids stay close to Anthropic's Java surface. The wrapper intentionally exposes a Clojure-shaped subset: maps in, maps out, keywords for roles and block types.

Installation

Leiningen (project.clj):

[net.clojars.savya/anthropic-clj "0.8.0"]

tools.deps (deps.edn):

net.clojars.savya/anthropic-clj {:mvn/version "0.8.0"}

Set ANTHROPIC_API_KEY in your environment, or pass client options: :api-key, :auth-token, :base-url, :timeout-ms, :max-retries.

Tracks com.anthropic/anthropic-java 2.48.0 - see CHANGELOG.md for the bump history.

Usage

(require '[anthropic.core :as anthropic])

(def client (anthropic/client))   ; reads ANTHROPIC_API_KEY

;; A single message. :model defaults to "claude-opus-4-8", :max-tokens to 1024.
(anthropic/create-message
  client
  {:model "claude-opus-4-8"
   :max-tokens 1024
   :system "You are concise."
   :messages [{:role :user :content "Name three primary colors."}]})
;; => {:id "msg_..." :model "claude-opus-4-8" :role :assistant
;;     :stop-reason :end-turn
;;     :content [{:type :text :text "Red, blue, yellow."}]
;;     :usage {:input-tokens 18 :output-tokens 9}}

create-message also accepts the optional controls :temperature, :top-p, :top-k, :stop-sequences, :tool-choice (:auto/:any/:none or {:type :tool :name "x"}), :thinking ({:type :enabled :budget-tokens N}, {:type :adaptive}, or {:type :disabled}), :metadata ({:user-id "..."}), :service-tier (:auto/:standard-only), :container, :inference-geo, :user-profile-id, and top-level :cache-control. For structured output, pass :response-format and/or :effort. Responses include newer :usage fields when present, including cache creation/read tokens, server-tool usage, service-tier, inference geo, cache creation details, and output-token details.

Images, PDFs, and prompt caching

Message content can be a vector of blocks. Beyond :text, :tool-use, and :tool-result, you can send :image, :document, :search-result, :thinking, :redacted-thinking, and :container-upload blocks. Blocks that support prompt caching accept :cache-control.

(anthropic/create-message
  client
  {:max-tokens 256
   :messages [{:role :user
               :content [{:type :image
                          :source {:type :base64 :media-type "image/png" :data "<base64>"}}
                         ;; or {:type :url :url "https://…/photo.jpg"}
                         {:type :document
                          :source {:type :url :url "https://…/paper.pdf"}
                          :title "Paper"}
                         {:type :search-result
                          :source "https://example.com/result"
                          :title "Result"
                          :citations true
                          :content [{:type :text :text "Relevant excerpt"}]}
                         {:type :text :text "Summarize the paper and the image."
                          :cache-control true}]}]})  ; :cache-control {:ttl :1h} for 1-hour

Assistant turns that contained thinking can be round-tripped with {:type :thinking :thinking "..." :signature "..."} or {:type :redacted-thinking :data "..."}. Container uploads use {:type :container-upload :file-id "file_..."}.

Server-side tools

Enable Anthropic-hosted tools by :type (latest version of each is used). The model runs them server-side; the response content carries :server-tool-use blocks and typed result blocks (:web-search-result, :code-execution-result, …).

(anthropic/create-message
  client
  {:max-tokens 1024
   :tools [{:type :web-search :max-uses 3
            :allowed-domains ["clojure.org"]        ; or :blocked-domains
            :user-location {:city "Paris" :country "FR"}
            :allowed-callers [:direct]}             ; some models need :direct
           {:type :web-fetch :max-content-tokens 4096}
           {:type :code-execution}
           {:type :bash}
           {:type :text-editor :max-characters 2000}
           {:type :memory}
           {:type :tool-search :variant :bm25}   ; or :regex
           {:type :tool-search :variant :regex
            :defer-loading true :strict true
            :allowed-callers [:direct]}]
   :messages [{:role :user :content "Search the web for today's Clojure news."}]})

Structured output

Pass :response-format (a JSON Schema map) to get a :parsed Clojure map back. Object schemas must set "additionalProperties": false (an API requirement). :effort (:low:max) is accepted alongside or on its own.

(anthropic/create-message
  client
  {:max-tokens 256
   :response-format {:type "object"
                     :properties {:capital {:type "string"}}
                     :required ["capital"]
                     :additionalProperties false}
   :messages [{:role :user :content "What is the capital of France?"}]})
;; => {... :content [{:type :text :text "{\"capital\":\"Paris\"}"}]
;;     :parsed {:capital "Paris"}}

Counting tokens

count-tokens takes the same request map and returns the input-token count without sending the message (:max-tokens and sampling params are ignored).

(anthropic/count-tokens
  client
  {:messages [{:role :user :content "How many tokens is this?"}]})
;; => {:input-tokens 13}

Models

(anthropic/list-models client)
;; => [{:id "claude-opus-4-8" :display-name "Claude Opus 4.8"
;;      :created-at "2026-..." :max-tokens 64000} ...]

(anthropic/get-model client "claude-opus-4-8")
;; => {:id "claude-opus-4-8" :display-name "Claude Opus 4.8" ...}

Files (beta)

(def f (anthropic/upload-file client "paper.pdf"))   ; path/File/Path/InputStream/bytes
;; => {:id "file_..." :filename "paper.pdf" :mime-type "application/pdf"
;;     :size-bytes 12345 :created-at "2026-..."}

(anthropic/get-file client (:id f))
(anthropic/list-files client)
(anthropic/download-file client some-id)   ; bytes; only API-generated downloadable files
(anthropic/delete-file client (:id f))

Message Batches

Submit many requests at the 50%-cost batch tier. Each request is {:custom-id "..." :params <same map as create-message>}.

(def batch
  (anthropic/create-batch
    client
    [{:custom-id "a" :params {:max-tokens 64 :messages [{:role :user :content "Hi"}]}}
     {:custom-id "b" :params {:max-tokens 64 :messages [{:role :user :content "Bye"}]}}]))
;; => {:id "msgbatch_..." :processing-status :in-progress
;;     :request-counts {:processing 2 :succeeded 0 ...} ...}

(anthropic/get-batch client (:id batch))     ; poll until :processing-status :ended
(anthropic/list-batches client)
(anthropic/cancel-batch client (:id batch))

;; Once ended, pull results (succeeded entries carry the parsed :message):
(anthropic/batch-results client (:id batch))
;; => [{:custom-id "a" :result {:type :succeeded :message {...}}} ...]

;; Streaming reduction for large result sets:
(anthropic/reduce-batch-results client (:id batch)
  (fn [acc result] (conj acc (:custom-id result)))
  [])

Streaming

stream-text calls your callback with each text delta and returns the full text.

(anthropic/stream-text
  client
  {:model "claude-opus-4-8" :max-tokens 256
   :messages [{:role :user :content "Write a haiku about parentheses."}]}
  #(print %))   ; prints each delta as it arrives
;; => returns the complete string when the stream ends

For thinking or tool-use streams, stream surfaces every normalized event. Each event is a map keyed by :type: :message-start, :content-block-start (:index, :block), :text-delta/:thinking-delta/:input-json-delta/ :signature-delta (:index plus payload), :content-block-stop (:index), :message-delta (:stop-reason), and :message-stop. It still returns the full text. To rebuild a streamed tool call, accumulate :input-json-delta :partial-json per :index - the matching :content-block-start carries the tool :id/:name.

(anthropic/stream
  client
  {:max-tokens 256 :messages [{:role :user :content "Think, then answer."}]}
  (fn [ev]
    (case (:type ev)
      :thinking-delta (print "[thinking]" (:thinking ev))
      :text-delta     (print (:text ev))
      nil)))

Tool use

Declare tools as maps; tool_use blocks come back parsed, and you complete the loop by echoing the assistant turn and sending a :tool-result block.

(def weather-tool
  {:name "get_weather"
   :description "Get the current weather for a city"
   :input-schema {:type "object"
                  :properties {:city {:type "string"}}
                  :required ["city"]}})

(def ask {:role :user :content "What's the weather in Paris?"})

(def r1 (anthropic/create-message
          client {:tools [weather-tool] :messages [ask]}))

;; r1 :stop-reason is :tool-use; find the call:
(def call (first (filter #(= :tool-use (:type %)) (:content r1))))
;; => {:type :tool-use :id "toolu_..." :name "get_weather" :input {:city "Paris"}}

;; Run the tool yourself, then send the result back:
(anthropic/create-message
  client
  {:tools [weather-tool]
   :messages [ask
              {:role :assistant :content (:content r1)}
              {:role :user :content [{:type :tool-result
                                      :tool-use-id (:id call)
                                      :content "18°C and sunny"}]}]})
;; => {... :content [{:type :text :text "It's 18°C and sunny in Paris."}]}

What's covered

  • create-message - request map ↔ response map, with the full set of request controls (sampling, stop sequences, tool-choice, thinking, metadata, service-tier), cache-token usage, and structured output (:response-format:parsed, plus :effort)
  • Content blocks - text, tool_use/tool_result, images (base64/url), documents/PDFs (base64/url/text), search results, thinking/redacted-thinking round-trips, container uploads, and :cache-control breakpoints where supported
  • Tools - custom tools, plus server-side tools (web search, web fetch, code execution, bash, text editor, memory, tool-search bm25/regex), with :server-tool-use and typed result blocks parsed back out
  • Citations - text blocks carry :citations (char/page/content-block/ web-search/search-result locations) when present
  • count-tokens - input-token count without sending
  • stream-text - incremental text deltas
  • stream - every normalized stream event (message + content-block lifecycle, text/thinking/tool-use/signature deltas)
  • list-models / get-model - Models API
  • Message Batches - create-batch, get-batch, list-batches, cancel-batch, delete-batch, batch-results, reduce-batch-results
  • Files (beta) - upload-file, get-file, list-files, download-file, delete-file

Wrapped surfaces: Messages, streaming, tool use including server tools, Message Batches, Files beta, Models, and count-tokens. Not wrapped: beta platform surfaces such as Agents, Sessions, Skills, Memory Stores, webhooks, and other parallel Beta* APIs; reach for the Java SDK directly for those.

Errors

All failures throw ex-info keyed :anthropic/error in ex-data:

  • Request-shaping errors (bad tool spec, missing key) throw before any network call, with an error keyword describing the problem.
  • API failures carry {:anthropic/error :api-error :status <http status> :error-type <kw>} where :error-type is one of :bad-request, :unauthorized, :permission-denied, :not-found, :unprocessable-entity, :rate-limit, :internal-server, or :unexpected-status. The original SDK exception is preserved as (ex-cause e).
  • Network/IO failures carry {:anthropic/error :io-error}, original exception as cause.

Other SDK exceptions (e.g. AnthropicInvalidDataException) propagate unchanged.

Bedrock and Vertex

The SDK ships separate backend artifacts, com.anthropic/anthropic-java-bedrock and com.anthropic/anthropic-java-vertex, for Amazon Bedrock and Google Vertex AI. client here builds the direct-API client only, but every function takes the client as its first argument, so an AnthropicClient constructed from either backend artifact works with all of them.

Tests

Unit tests (the request/response translation) run with no network:

lein test

The :integration suite hits the live API and is billed - it needs ANTHROPIC_API_KEY and is run explicitly:

ANTHROPIC_API_KEY=sk-... lein test :integration

License

Copyright © 2026 Savyasachi

Distributed under the Eclipse Public License 2.0. The wrapped com.anthropic/anthropic-java SDK is MIT-licensed and remains the property of Anthropic.

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close