A Clojure SDK for LLM providers: one canonical API for chat, embeddings, moderation, rerank, image generation, audio transcription, and text-to-speech.
This is a provider SDK, not an agent framework or proxy server. It owns provider wire-format differences so your application does not have to. It does not include credential pools, budget routing, plugin loading, vector stores, MCP clients, observability sinks, or secret managers.
Add the library coordinate to deps.edn:
{:deps {net.clojars.deadmeme5441/clojure-llm-sdk {:mvn/version "0.6.0"}}}
Or with Leiningen / project.clj:
[net.clojars.deadmeme5441/clojure-llm-sdk "0.6.0"]
Upgrading to 0.6.0: review the migration notes for provider, streaming, registry, and model-resolution changes. Upgrading from 0.4.x also requires the 0.5.0 migration.
Then require the public namespace:
(require '[llm.sdk :as sdk])
(sdk/complete
:openai
{:request/model "gpt-4o-mini"
:request/messages [{:message/role :user
:message/content "Reply with the single word: ok"}]})
Responses use the same canonical shape across providers:
{: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/provider-data {...}
:response/raw {...}}
Streaming uses the same request shape:
(sdk/complete
:openai
{:request/model "gpt-4o-mini"
:request/messages [{:message/role :user
:message/content "Count to three"}]}
:stream? true
:on-event (fn [event]
(when (= :stream/content-delta (:event/type event))
(print (:event/delta event)))))
Without :on-event, streaming returns an llm.sdk.StreamHandle: a Seqable,
Closeable, reducible stream owner. Use with-open when consuming it as a
sequence:
(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)))))
reduce also closes the stream, including on early reduced termination.
With :on-event, the callback runs incrementally and sdk/complete returns
the accumulated canonical response after the stream ends.
The handle is single-pass and single-consumer: seq and reduce advance the
same cursor, and consumed events are released rather than retained for replay.
Retain any events your application needs later; do not consume one handle
concurrently.
Library docs:
unravel-team/litellm-clj library.Project docs:
The public API exposes these canonical surfaces:
| Modality | Public function | Providers |
|---|---|---|
| Chat | sdk/complete | OpenAI, Anthropic, Anthropic on Vertex, Gemini, Vertex Gemini, Z.AI, OpenRouter, Codex, DeepSeek, Kimi, Kimi Code, Mistral, Groq, Cerebras, Together, xAI, HuggingFace Router, Perplexity, Bedrock, Ollama, and aggregator aliases |
| Embeddings | sdk/embed | OpenAI, Gemini Native, OpenRouter, Azure OpenAI deployment profiles, Cohere, Voyage, Mistral, Together, Jina, Nebius, and Ollama |
| Moderation | sdk/moderate | OpenAI |
| Rerank | sdk/rerank | Cohere, Voyage, Jina |
| Image generation | sdk/generate-image | OpenAI, OpenRouter, Vertex Gemini image generation (:vertex-imagen), and Bedrock image models |
| Audio transcription | sdk/transcribe | OpenAI Whisper, Groq Whisper |
| Text-to-speech | sdk/speak | OpenAI TTS, ElevenLabs |
See Providers for the full provider matrix and credential list. These rows describe implemented constructors, not a promise that every catalog model is currently served or enabled for an account or region.
Offline model and pricing lookup merges bundled LiteLLM and models.dev snapshots. Entries retain source provenance and, when available, revision, freshness, availability, all contributing sources, and the pricing source. Missing usage or rates remain unknown rather than becoming zero or borrowing a rate from another modality. Live listing is available only for providers with a supported model-list endpoint and reports that endpoint's account-visible view; a failed refresh retains the previous live slice and marks it stale.
OpenAI, OpenRouter, and Bedrock image calls require an explicit
:image/model; none of these providers selects a billable image model
implicitly.
Credentials are read from environment variables. The SDK does not load .env files itself.
export OPENAI_API_KEY=sk-...
export ANTHROPIC_API_KEY=sk-ant-api03-...
export GEMINI_API_KEY=AIza...
export ZAI_API_KEY=...
export KIMI_API_KEY=...
Use .env.example as a non-secret template for local live smoke tests.
Some provider names are intentionally distinct:
:kimi uses Moonshot's public API and reads MOONSHOT_API_KEY.:kimi-code uses Kimi Code's coding endpoint and reads KIMI_API_KEY.:vertex-gemini uses Google Application Default Credentials or GOOGLE_OAUTH_ACCESS_TOKEN.:zai uses Z.AI's OpenAI-compatible GLM endpoint and reads ZAI_API_KEY.:vertex-anthropic serves Claude models through Google Vertex AI using the same GCP credentials as :vertex-gemini, not an ANTHROPIC_API_KEY.:codex-backend uses ChatGPT OAuth over HTTP/SSE (default) or Responses WebSocket V2 (:config {:transport :websocket}). Managed Codex CLI file credentials refresh automatically; caller-managed :auth-token and :account-id bypass file storage. See OAuth configuration and live verification. :openai and API-key :codex remain separate.Applications that manage secrets outside environment variables can pass per-call runtime config:
(sdk/complete :openai request
:config {:api-key "sk-..."
:base-url "https://api.openai.com/v1"
:timeout-ms 60000})
:unknown, never zero; reported token totals and costs remain authoritative.:http-client plus connect and request timeouts.Embeddings:
(sdk/embed
:openai
{:embed/model "text-embedding-3-small"
:embed/inputs ["clojure" "lisp" "java"]})
Native Gemini embeddings use the existing :gemini-native profile:
(sdk/embed
:gemini-native
{:embed/model "gemini-embedding-001"
:embed/inputs ["clojure" "lisp" "java"]
:embed/dimensions 256
:embed/provider-options {:task-type :retrieval-document}})
Rerank:
(sdk/rerank
:cohere
{:rerank/model "rerank-english-v3.0"
:rerank/query "lisp dialect on the JVM"
:rerank/documents ["Python" "Clojure" "JavaScript"]
:rerank/top-n 3})
Fallbacks:
(sdk/with-fallbacks
[[:openai "gpt-4o"]
[:anthropic "claude-haiku-4-5"]
[:groq "llama-3.1-8b-instant"]]
{:request/messages [{:message/role :user
:message/content "Reply: ok"}]})
Custom OpenAI-compatible alias:
(require '[llm.sdk.providers.openai.chat :as openai-chat])
(openai-chat/register-alias!
{:id :my-private-llm
:base-url "https://llm.example.com/v1"
:env-var-names ["MY_PRIVATE_LLM_KEY"]
:capabilities #{:chat :streaming :tools}})
Provider-family namespaces such as llm.sdk.providers.openai.chat, llm.sdk.providers.anthropic.chat, llm.sdk.providers.gemini.embeddings, llm.sdk.providers.zai.chat, and llm.sdk.providers.openrouter.chat are the sole provider implementations. The former flat namespaces have been removed.
The public CI workflow runs the offline checks on JDK 17 and 21:
clj-kondo --lint src test
clojure -M:test
python3 -m unittest discover -s test -p '*_test.py'
clojure -T:build jar
Release publishing uses one build task and one RELEASE_VERSION for the jar,
POM, and deployment:
RELEASE_VERSION=x.y.z clojure -T:build deploy
Live tests are explicit:
cp .env.example .env
set -a; source .env; set +a
clojure -M:live-test
MIT
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 |