This guide is for application authors using clojure-llm-sdk as a library. The SDK normalizes provider wire formats into one Clojure surface while preserving provider-specific replay state such as reasoning signatures, encrypted reasoning items, citations, and tool-call ids.
Add the same Maven coordinate used by the README to deps.edn:
{:deps {net.clojars.deadmeme5441/clojure-llm-sdk
{:mvn/version "0.6.0"}}}
Then require the public namespace:
(require '[llm.sdk :as sdk])
Set only the credentials for providers you call. For local live smoke tests, copy the template:
cp .env.example .env
Example shell setup:
export OPENAI_API_KEY=sk-...
export ANTHROPIC_API_KEY=sk-ant-api03-...
export KIMI_API_KEY=...
The SDK does not load .env files by itself. Your application or shell should load them before invoking the library.
(sdk/complete
:openai
{:request/model "gpt-4o-mini"
:request/messages [{:message/role :user
:message/content "Reply with the single word: ok"}]})
The response is canonical:
{:response/provider :openai
:response/model "gpt-4o-mini"
:response/parts [{:part/type :text
:text "ok"}]
:response/finish-reason :stop
:response/usage {...} ; optional; reported totals remain authoritative
:response/cost {...} ; optional; provider-reported cost wins
:response/cache {...} ; hit/miss/unknown when stamped
:response/raw {...}}
See canonical-response.md for the full response contract.
Pass :stream? true. Without a callback, sdk/complete returns an
llm.sdk.StreamHandle implementing Seqable, java.io.Closeable,
clojure.lang.IReduce, and clojure.lang.IReduceInit:
(with-open [events (sdk/complete
:openai
{:request/model "gpt-4o-mini"
:request/messages
[{:message/role :user
:message/content "Count to three"}]}
:stream? true)]
(doseq [event events]
(when (= :stream/content-delta (:event/type event))
(print (:event/delta event)))))
Use with-open for sequence operations that may stop before EOF. Direct
reduce closes the stream after full consumption, errors, or early reduced
termination.
The handle is single-pass and single-consumer: sequence operations and
reduce advance the same cursor, and consumed events are released. Retain any
events you need later instead of trying to replay or concurrently consume the
same handle.
Alternatively, pass :on-event. The callback runs once for each canonical
event as it arrives, and sdk/complete returns the accumulated response after
the terminal event:
(def response
(sdk/complete
:openai
{:request/model "gpt-4o-mini"
:request/messages [{:message/role :user
:message/content "Count to three"}]}
:stream? true
:on-event #(when (= :stream/content-delta (:event/type %))
(print (:event/delta %)))))
Stream events use a provider-neutral taxonomy: content deltas, reasoning deltas, tool-call deltas, citations, usage, provider-state, errors, and end events. Reported usage, cost, and totals remain authoritative; fields the provider omits remain absent or unknown.
Tools use OpenAI-style function tool shape at the canonical layer:
(def weather-tool
{:type :function
:function {:name "get_weather"
:description "Get current weather for a location"
:parameters {:type :object
:required ["location"]
:properties {:location {:type :string}}}}})
(def first-turn
(sdk/complete
:openai
{:request/model "gpt-4o"
:request/messages [{:message/role :user
:message/content "Weather in New York?"}]
:request/tools [weather-tool]}))
When :response/finish-reason is :tool-calls, execute the requested tool in your application and send a tool result message:
(let [call (first (:response/tool-calls first-turn))]
(sdk/complete
:openai
{:request/model "gpt-4o"
:request/messages
[{:message/role :user
:message/content "Weather in New York?"}
{:message/role :assistant
:message/content ""
:message/tool-calls (:response/tool-calls first-turn)}
{:message/role :tool
:message/content
[{:part/type :tool-result
:tool-result/id (:tool-call/id call)
:tool-result/name (:tool-call/name call)
:tool-result/content "{\"temperature\":72,\"condition\":\"clear\"}"
:tool-result/is-error false}]}]
:request/tools [weather-tool]}))
The SDK preserves provider-specific tool-call ids and replay fields so multi-turn requests can be reconstructed correctly.
Typed tool results keep id, name, content, and error status together. Providers
with a native error-status field preserve :tool-result/is-error; providers
without one reject an error result rather than silently sending it as success.
The same public namespace exposes non-chat modalities:
(sdk/embed :openai
{:embed/model "text-embedding-3-small"
:embed/inputs ["clojure" "lisp"]})
(sdk/moderate :openai
{:moderation/inputs ["text to classify"]})
(sdk/rerank :cohere
{:rerank/model "rerank-english-v3.0"
:rerank/query "JVM Lisp"
:rerank/documents ["Python" "Clojure" "JavaScript"]})
(sdk/generate-image :openai
{:image/model "dall-e-3"
:image/prompt "a product photo of a brass desk lamp"})
(sdk/transcribe :openai
{:transcribe/model "whisper-1"
:transcribe/file (java.io.File. "clip.wav")})
(sdk/speak :openai
{:speak/model "tts-1"
:speak/voice "alloy"
:speak/input "Hello"})
See api-reference.md for the request keys each modality accepts.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |